binding to custom element is not working in aurelia - aurelia

I want to use a custom element, bind values to it, check and manipulate them in the view model and show them in the view.
This is the container's relevant statements:
<require from = "./../userAccount/userAccount"></require>
<div class="UserAccountWrapper" repeat.for="userAccount of
userAccountsData">
<div><user-account accountDetails.bind="userAccount"></user-account></div>
</div>
This is the custom element relevant code:
import { bindable } from 'aurelia-framework';
export class UserAccount{
#bindable account;
constructor() {}
activate(account) {
this.accountDetails.CompanyName = account.CompanyName == null ? "N/A" :
account.CompanyName;
....
}
The data doesn't get binded to the custom element.

You need to use the bind callback. activate is a callback for routed pages (and some other cases I mentioned in a comment to your question). Also, the bound value is not passed as a parameter to the bind callback, but it will be set at that point. You do have to use this before the property name though.
import { bindable } from 'aurelia-framework';
export class UserAccount{
#bindable account;
bind() {
this.accountDetails.CompanyName = this.account.CompanyName == null ? "N/A" :
this.account.CompanyName;
....
}
}
Another choice is to use the ${propertyName}Changed callback. This function will be called every time the property changes. Note that this is only when the property itself changes, and not when properties on the bound property change. So you the callback won't happen if only this.account.CompanyName changes. The changed callback will provide you with the new value and old value of the property as parameters to the callback, but you can ignore the parameters if you don't want them.
import { bindable } from 'aurelia-framework';
export class UserAccount{
#bindable account;
accountChanged(newValue, oldValue) {
this.accountDetails.CompanyName = account.CompanyName == null ? "N/A" :
this.account.CompanyName;
// OR
this.accountDetails.CompanyName = newValue || "N/A";
....
}
}

Related

Extending product model in Spartacus

I'm trying to extend the product model with a new attribute I created in the -items.xml, but I can't seem to get it in Spartacus front. I added a product.model.ts file with the following code:
import { Product as CxProduct } from '#spartacus/core';
export interface Product extends CxProduct {
myAttribute: string;
}
And I imported that file in the pipe where I use the product model, but it appears empty althoughs the product does have data in myAttribute field in backoffice. Is there something I may be missing?
I'm using this attribute in a product-images.component I created to override the default one.
This is the new product-images.component.ts
import { Component, OnInit } from '#angular/core';
import { CurrentProductService } from '#spartacus/storefront';
import { ProductImagesComponent as SpartacusProductImages } from '#spartacus/storefront'
#Component({
selector: 'cx-product-images',
templateUrl: './product-images.component.html',
styleUrls: ['./product-images.component.scss']
})
export class ProductImagesComponent extends SpartacusProductImages implements OnInit {
constructor(currentProductService: CurrentProductService) {
super(currentProductService)
}
ngOnInit() {
}
}
Thanks a lot
You need ask backend explicitly to return you the attribute. Please configure the product endpoint to contain ?fields= with your custom attribute. See docs: https://sap.github.io/spartacus-docs/connecting-to-other-systems/#configuring-endpoints
Please mind that you can specify various scoped endpoint configuraitons for product data - so you can load the custom attribute only when needed (i.e. it's useful when myAttribute is not displayed for some views like product carousel or product list). But since you're manipulating the image data, I guess you want to use this property everywhere (so please use scope called list).
For more, see docs for scoped product endpoint: https://sap.github.io/spartacus-docs/loading-scopes/#using-default-product-scopes. Note: When writing this post, the docs for Spartacus 2.0 version are not yet published. In 2.0 the product_scopes config property was renamed to product.

Aurelia custom Binding Behavior to observe ALL properties of an object

I would like to create a custom Binding Behavior that allows me to detect ANY changes to the properties of an object, like this:
<my-form model.bind="myObject & objectObserver:myObjChanged()"></my-form>
I know that I can use Aurelia's binding engine to create a property observer, and perhaps I can build this into a custom Binding Behavior to detect the properties of the object and create property observers for each one. But I can't make sense of the binding object that is given to me inside the custom Binding Behavior. Here's my code so far:
import { inject, bindingBehavior, BindingEngine } from 'aurelia-framework';
#bindingBehavior('objectObserver')
#inject(BindingEngine)
export default class ObjectObserverBindingBehavior {
constructor(bindingEngine) {
this.bindingEngine = bindingEngine;
}
bind(binding, scope, interceptor) {
console.warn('hello', binding, scope, interceptor);
}
unbind(binding, scope) {
console.warn('observer.unbind()', binding, scope);
}
}
When the bind happens and the console text is output, I see
So I know it's working, but I don't know what the best object is to start watching. I see the bound object inside targetObserver.currentValue. Is that the best property to start watching? Is there another way that utilizes existing functionality of the Aurelia Binding Engine?
I found a solution that is not Aurelia specific, based on the Proxy functionality built into Javascript.
export function onChangeObj(object, onChange) {
// creates Proxy to detect changes in object properties and call a function
if (typeof onChange !== 'function' || typeof object !== 'object') {
throw new Error('onChangeObj: incorrect parameters');
}
const handler = {
set(obj, prop, value) {
onChange(prop, value);
return Reflect.set(obj, prop, value);
},
};
return new Proxy(object, handler);
}
In order to use it, just call it like this:
this.myObject = onChangeObj(this.myObject, () => this.myObjChanged());
Effectively, the object is replaced by a wrapper Proxy that calls the provided function every time one of the properties is modified (with the setter).
If anyone finds a solution via Aurelia Binding Behavior, I would still be interested.

Aurelia: Issue with using a shared class specification for properties in parent and child custom elements

I have a code like below where a Project has a Timeline.
I have created a class named TimelineProperties which acts as a shared specification for a properties I want to pass from a project to a child Timeline.
I have planned to use TimelineProperties class in Project custom element like this:
import {TimelineProperties} from "./timeline";
#customElement('project')
export class Project {
timeline: TimelineProperties = new TimelineProperties();
}
<template>
...
<timeline list.bind="timeline.list" loading.bind="timeline.loading" error.bind="timeline.error" />
...
</template>
And also inside a Timeline custom element (to share code):
Either via inheritance like this:
export class Timeline extends TimelineProperties {}
Or via composition like this:
export class Timeline {
// TimelineProperties class has #bindable properties defined
timeline: TimelineProperties = new TimelineProperties();
}
// and then use bindings from project.html like this:
<timeline timeline.list.bind="timeline.list" timeline.loading.bind="timeline.loading" timeline.error.bind="timeline.error" />
The issue is that I cannot use a shared specification class TimelineProperties inside a Timeline custom element either via inheritance nor composition.
Issue with inheritance - https://github.com/aurelia/templating/pull/507
Issue with composition - Exception in runtime: "Error: Attempted to register an Element when one with the same name already exists. Name: project"
So now I have copied the TimelineProperties fields also into a Timeline Custom Element class (see 3 #bindable properties inside timeline.ts code below) just to make it work. But I would like to avoid that code duplication.
My question is, is there some way I could use TimelineProperties class inside a Timeline Custom Element to bind data from Project Element directly into a Timeline's TimelineProperties?
Here is my full code that works by code duplication and not using a shared TimelineProperties class:
project.ts - I have a parent Custom Element like this:
import {TimelineProperties} from "./timeline";
#customElement('project')
export class Project {
timeline: TimelineProperties = new TimelineProperties();
}
<template>
...
<timeline list.bind="timeline.list" loading.bind="timeline.loading" error.bind="timeline.error" />
...
</template>
timeline.ts - And child Custom Element like this:
import {DataLoading} from "./api";
export class TimelineProperties extends DataLoading {
#bindable list: Task[] = [];
}
#customElement('timeline')
export class Timeline {
#bindable list: Task[] = [];
#bindable loading: boolean = false;
#bindable error: any;
...
}
<template>
...
</template>
api.ts
export class DataLoading {
#bindable loading: boolean = false;
#bindable error: any;
}
UPDATE - Satisfied with this solution
Based on Ashley Grant's suggestions to use a decorator to initialize the bindings, I have modified my code based on that input and I am happy with it now. Here is my code now using the "extends" and a decorator to initialize the bindings in child classes:
import {DataLoading} from "./api";
export class TimelineProperties extends DataLoading {
list: Task[] = [];
}
// To ensure a compile time errors if property names are changed in TimelineProperties
function propertyName<T>(name: keyof T){
return name;
}
// Now define all bindings in a decorator instead of inside the classes
function timelineBindings() {
return function(target) {
bindable(propertyName<TimelineProperties>("loading"))(target);
bindable(propertyName<TimelineProperties>("error"))(target);
bindable(propertyName<TimelineProperties>("list"))(target);
}
}
#customElement('timeline')
#timelineBindings()
export class Timeline extends TimelineProperties {
...
}
I'm not sure why you are trying to create such a deep hierarchy of classes for this. These classes have one or two properties, so I'm not seeing what the benefit is of the inheritance tree. You're writing TypeScript, so you'd probably be better served by using interfaces for this stuff.
How many other places is TimelineProperties used in your codebase? If this is the only place then there isn't really code duplication in reality. The code duplication is caused by your insistence on using a bunch of classes to unnecessarily complicate the design.
I'm sorry if I'm misreading this from your example code, but I very often see developers going crazy with inheritance and such thinking it will help them not repeat themselves, but in the end the benefits of DRY are vastly outweighed by the increased complexity of the solution they produce.
So anyways, assuming there actually is a need to reuse these classes in multiple places, I would recommend using decorators to accomplish this.
For example: https://gist.run/?id=6ad573e051f53c8e163d36dc31dc36b6
timeline-props.js
import {bindable} from 'aurelia-framework';
export function timelineProps() {
return function(target) {
bindable('list')(target);
}
}
timeline.js
import {timelineProps} from 'timeline-props';
#timelineProps()
export class Timeline {
}
timeline.html
<template>
<ul>
<li repeat.for="item of list">
${item}
</li>
</ul>
</template>
app.html
<template>
<require from="./timeline"></require>
<timeline list.bind="items"></timeline>
</template>
This is the exact type of use-case that decorators were created to handle.

How to use static methods in view templates

I want to use static class constants in my view template
Javascript
class FilterModel {
static const FILTER_TYPE_STRING() {
return 'string';
}
}
HTML
<div show.bind="selectedFacet.type===FilterModel.FILTER_TYPE_STRING">
</div>
Short answer: the binding language does not support that.
I met the problem before, but didn't find a good workaround. In such case, I usually define a constant, which the class would set the value from in the constructor and add a comment to show it should be static.
const _filterTypeString: string = 'string';
export class FilterModel {
/*static*/ filterTypeString: string = _filterTypeString;
}
Longer answer: static members in JavaScript are being transpiled directly into the class, not into the prototype, so instances do not have reference to it. The difference is:
class MyClass {
instanceMember: number = 256
static staticMember: number = 1024;
}
//referencing them:
MyClass.prototype.instanceMember
MyClass.staticMember
Everytime when you create an object instance, it creates a copy of the prototype, so that this would have equal values to the prototype. That's what Aurelia does also, creates an instance, when creating a view-model.
In the view template, you can only access members of that object, which is inherited from the prototype. Since the static method is not a member of that object, you cannot access it. Of course, you might create a reference, but I found this workaround more annoying than the one above. With code example:
export class FilterModel {
static filterTypeString: string = 'string';
refToFilterTypeString = FilterModel.filterTypeString;
}
HTML:
<div show.bind="selectedFacet.type === refToFilterTypeString">
</div>

Yii: attaching events to models

I have a User model which is bundled in a module installed on my Yii application. This module is third party and I do not want to alter its code.
I also have a Cv Model that has a BELONGS_TO relation with the User model.
My question is: How can I delete the cv when a user is deleted ?
I know that I can achieve this with on delete cascade ... on mysql. However, i do need to delete other data such as a photo, files, etc.
What I have tried
I have created a component that is preloaded on my application. This component attaches to an onAfterDelete event
class EventListener extends CComponent
{
public function init() {
Yii::import("application.modules.users.models.User");
User::model()->attachEventHandler('onAfterDelete', array($this, 'deleteUser'));
}
public function deleteUser($event)
{
// stuff here ...
}
}
However this does not work.
Any suggestions ?
This may help you.
User::model() is a singleton
$user1 = User::model();
$user2 = new User; // will be used in insert action
$user3 = User::model()->findByPk(10); // will be used in update/delete action
$user1, $user2 and $user3 are completely different objects.
You can attach events to objects, in this case you have to add events to all these 3 objects individually.
$user1->attachEventHandler(...);
$user2->attachEventHandler(...);
$user3->attachEventHandler(...);
look like Yii does not provide any way to add events at Class level.
Well, guys, I have just stumbled upon the same problem and I solved it this way:
You should use the init() of a Model, not your event listener collection class.
In my case I have devModel class:
public function init()
{
parent::init();
$this->onLicenseUpdated = array(new MEventProcessor, 'licenseUpdateHandler');
}
And the handler is licenseUpdateHandler($event) in a MEventProcessor class.
This way every time you work with model instance, it'll call init() for every object and attach the event handler to every instance of this Model.
Now any time the event (in my case onLicenseUpdated()) is invoked for the model - the handler will be called too.
You could also to use Behaviors.
1 - behaviors can listen to events : you just have to override their events() method
class MyBehavior extends Behavior {
public function events() {
return [
ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
];
}
public function beforeValidate($event) {
// ...
}
}
2 - although it is more common to attach a behavior to a component in the component's behaviors() method, you can also attach them dynamically and keep the original code unmodified :
use app\components\MyBehavior;
// attach a behavior object
$component->attachBehavior('myBehavior1', new MyBehavior);
You will find some useful documentation here :
yii 1 : http://www.yiiframework.com/doc/api/1.1/CBehavior
yii 2 : http://www.yiiframework.com/doc-2.0/guide-concept-behaviors.html