This is my parent view model and view.
export class Parent {
#observable field;
fieldChanged() {
console.log('field has been changed');
}
}
<template>
<child-component field.two-way="field" />
</template>
When I do
this.field.property = 'new value';
in child-component, fieldChanged method is not called.
Note that field is type of object. With primitive types it works well.
Can I do something to make this work on object types?
If you want to observe a property of an object, you can use the bindingEngine:
import { BindingEngine, inject } from 'aurelia-framework';
#inject(BindingEngine)
export class Parent {
field = {
property: ''
}
constructor(bindingEngine) {
this.bindingEngine = bindingEngine;
}
attached() {
this.subscription = this.bindingEngine.propertyObserver(this.field, 'property')
.subscribe((newValue, oldValue) => {
// do your logic here
})
}
detached() {
// Dispose subscription to avoid memory leak
this.subscription.dispose();
}
}
You can use BindingEngine.expressionObserver method to observe a path, instead of a single property
const observer = bindingEngine
.expressionObserver(this /* or any object */, 'field.property')
.subscribe(newValue => console.log('new field.property is:', newValue))
And remember to call observer.dispose() later when no longer needed.
you probably didn't declare the binding in the child component:
import {bindable} from 'aurelia-framework';
export class ChildComponent
{
#bindable field;
/* DO whatever you want*/
}
btw:
in your code you should have this.field = 'new value'; and not field = 'new value';
Related
The code starts with an initial value in product variable, which is setted into sessionStorage. When i trigger the side-panel (child component), this receive the product.name from params in url, then this component searchs in sessionStorage and updates the product.amount value (and set it to sessionStorage).
The parent component function that i'm trying to invoke from the child component is getProductStatus(); When i update the product.amount value in the side-panel i need to update also the product object in parent component at the same time. This is what i've been trying, Thanks in advance.
Code:
https://stackblitz.com/edit/angular-ivy-npo4z7?file=src%2Fapp%2Fapp.component.html
export class AppComponent {
product: any;
productReturned: any;
constructor() {
this.product = {
name: 'foo',
amount: 1
};
}
ngOnInit() {
this.getProductStatus();
}
getProductStatus(): void {
this.productReturned = this.getStorage();
if (this.productReturned) {
this.product = JSON.parse(this.productReturned);
} else {
this.setStorage();
}
}
setStorage(): void {
sessionStorage.setItem(this.product.name, JSON.stringify(this.product));
}
getStorage() {
return sessionStorage.getItem(this.product.name);
}
reset() {
sessionStorage.clear();
window.location.reload();
}
}
You have two options for data sharing in this case. If you only need the data in your parent component:
In child.component.ts:
#Output() someEvent = new EventEmitter
someFunction(): void {
this.someEvent.emit('Some data...')
}
In parent template:
<app-child (someEvent)="handleSomeEvent($event)"></app-child>
In parent.component.ts:
handleSomeEvent(event: any): void {
// Do something (with the event data) or call any functions in the component
}
If you might need the data in another component aswell, you could make a service bound to the root of the application with a Subject to subscibe to in any unrelated component wherever in your application.
Service:
#Injectable()
export class DataService {
private _data = new BehaviorSubject<SnapshotSelection>(new Data());
private dataStore: { data: any }
get data() {
return this.dataStore.asObservable();
}
updatedDataSelection(data: Data){
this.dataStore.data.push(data);
}
}
Just pass the service in both constructors of receiving and outgoing component.
In ngOnInit() on receiving side:
subscription!: Subscription
...
dataService.data.subscribe(data => {
// Do something when data changes
})
...
ngOnDestroy() {
this.subscription.unsubscribe()
}
Then just use updatedDataSelection() where the changes originate.
I documented on all types of data sharing between components here:
https://github.com/H3AR7B3A7/EarlyAngularProjects/tree/master/dataSharing
For an example on the data service:
https://github.com/H3AR7B3A7/EarlyAngularProjects/tree/master/dataService
I'm working with lit-elements via #open-wc and is currently trying to write a nested set of components where the inner component is an input field and some ancestor component has to support some arbitrary rewrite rules like 'numbers are not allowed input'.
What I'm trying to figure out is what the right way to built this is using lit-elements. In React I would use a 'controlled component' see here easily forcing all components to submit to the root component property.
The example below is what I've come up with using Lit-Elements. Is there a better way to do it?
Please note; that the challenge becomes slightly harder since I want to ignore some characters. Without the e.target.value = this.value; at level-5, the input elmement would diverge from the component state on ignored chars. I want the entire chain of components to be correctly in sync, hence the header tags to exemplify.
export class Level1 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h1>${this.value}</h1>
<level-2 value=${this.value} #input-changed=${this.onInput}></level-2>
</div>`;
}
onInput(e) {
this.value = e.detail.value.replace(/\d/g, '');
}
}
...
export class Level4 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h4>${this.value}</h4>
<level-5 value=${this.value}></level-5>
</div>`;
}
}
export class Level5 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h5>${this.value}</h5>
<input .value=${this.value} #input=${this.onInput}></input>
</div>`;
}
onInput(e) {
let event = new CustomEvent('input-changed', {
detail: { value: e.target.value },
bubbles: true,
composed: true
});
e.target.value = this.value;
this.dispatchEvent(event);
}
}
export class AppShell extends LitElement {
constructor() {
super();
this.value = 'initial value';
}
render() {
return html`
<level-1 value=${this.value}></level-1>
`;
}
}
Added later
An alternative approach was using the path array in the event to access the input element directly from the root component.
I think it's a worse solution because it results in a stronger coupling accross the components, i.e. by assuming the child component is an input element with a value property.
onInput(e) {
const target = e.path[0]; // origin input element
this.value = e.path[0].value.replace(/\d/g, '');
// controlling the child elements value to adhere to the colletive state
target.value = this.value;
}
Don't compose your events, handle them in the big parent with your logic there. Have the children send all needed info in the event, try not to rely on target in the parent's event handler.
To receive updates, have your components subscribe in a shared mixin, a la #mishu's suggestion, which uses some state container (here, I present some imaginary state solution)
import { subscribe } from 'some-state-solution';
export const FormMixin = superclass => class extends superclass {
static get properties() { return { value: { type: String }; } }
connectedCallback() {
super.connectedCallback();
subscribe(this);
}
}
Then any component-specific side effects you can handle in updated or the event handler (UI only - do logic in the parent or in the state container)
import { publish } from 'some-state-solution';
class Level1 extends LitElement {
// ...
onInput({ detail: { value } }) {
publish('value', value.replace(/\d/g, ''));
}
}
How to handle/provide #Input and #Output properties for dynamically created Components in Angular 2?
The idea is to dynamically create (in this case) the SubComponent when the createSub method is called. Forks fine, but how do I provide data for the #Input properties in the SubComponent. Also, how to handle/subscribe to the #Output events the SubComponent provides?
Example:
(Both components are in the same NgModule)
AppComponent
#Component({
selector: 'app-root'
})
export class AppComponent {
someData: 'asdfasf'
constructor(private resolver: ComponentFactoryResolver, private location: ViewContainerRef) { }
createSub() {
const factory = this.resolver.resolveComponentFactory(SubComponent);
const ref = this.location.createComponent(factory, this.location.length, this.location.parentInjector, []);
ref.changeDetectorRef.detectChanges();
return ref;
}
onClick() {
// do something
}
}
SubComponent
#Component({
selector: 'app-sub'
})
export class SubComponent {
#Input('data') someData: string;
#Output('onClick') onClick = new EventEmitter();
}
You can easily bind it when you create the component:
createSub() {
const factory = this.resolver.resolveComponentFactory(SubComponent);
const ref = this.location.createComponent(factory, this.location.length, this.location.parentInjector, []);
ref.someData = { data: '123' }; // send data to input
ref.onClick.subscribe( // subscribe to event emitter
(event: any) => {
console.log('click');
}
)
ref.changeDetectorRef.detectChanges();
return ref;
}
Sending data is really straigthforward, just do ref.someData = data where data is the data you wish to send.
Getting data from output is also very easy, since it's an EventEmitter you can simply subscribe to it and the clojure you pass in will execute whenever you emit() a value from the component.
I found the following code to generate components on the fly from a string (angular2 generate component from just a string) and created a compileBoundHtml directive from it that passes along input data (doesn't handle outputs but I think the same strategy would apply so you could modify this):
#Directive({selector: '[compileBoundHtml]', exportAs: 'compileBoundHtmlDirective'})
export class CompileBoundHtmlDirective {
// input must be same as selector so it can be named as property on the DOM element it's on
#Input() compileBoundHtml: string;
#Input() inputs?: {[x: string]: any};
// keep reference to temp component (created below) so it can be garbage collected
protected cmpRef: ComponentRef<any>;
constructor( private vc: ViewContainerRef,
private compiler: Compiler,
private injector: Injector,
private m: NgModuleRef<any>) {
this.cmpRef = undefined;
}
/**
* Compile new temporary component using input string as template,
* and then insert adjacently into directive's viewContainerRef
*/
ngOnChanges() {
class TmpClass {
[x: string]: any;
}
// create component and module temps
const tmpCmp = Component({template: this.compileBoundHtml})(TmpClass);
// note: switch to using annotations here so coverage sees this function
#NgModule({imports: [/*your modules that have directives/components on them need to be passed here, potential for circular references unfortunately*/], declarations: [tmpCmp]})
class TmpModule {};
this.compiler.compileModuleAndAllComponentsAsync(TmpModule)
.then((factories) => {
// create and insert component (from the only compiled component factory) into the container view
const f = factories.componentFactories[0];
this.cmpRef = f.create(this.injector, [], null, this.m);
Object.assign(this.cmpRef.instance, this.inputs);
this.vc.insert(this.cmpRef.hostView);
});
}
/**
* Destroy temporary component when directive is destroyed
*/
ngOnDestroy() {
if (this.cmpRef) {
this.cmpRef.destroy();
}
}
}
The important modification is in the addition of:
Object.assign(this.cmpRef.instance, this.inputs);
Basically, it copies the values you want to be on the new component into the tmp component class so that they can be used in the generated components.
It would be used like:
<div [compileBoundHtml]="someContentThatHasComponentHtmlInIt" [inputs]="{anInput: anInputValue}"></div>
Hopefully this saves someone the massive amount of Googling I had to do.
createSub() {
const factory = this.resolver.resolveComponentFactory(SubComponent);
const ref = this.location.createComponent(factory, this.location.length,
ref.instance.model = {Which you like to send}
ref.instance.outPut = (data) =>{ //will get called from from SubComponent}
this.location.parentInjector, []);
ref.changeDetectorRef.detectChanges();
return ref;
}
SubComponent{
public model;
public outPut = <any>{};
constructor(){ console.log("Your input will be seen here",this.model) }
sendDataOnClick(){
this.outPut(inputData)
}
}
If you know the type of the component you want to add i think you can use another approach.
In your app root component html:
<div *ngIf="functionHasCalled">
<app-sub [data]="dataInput" (onClick)="onSubComponentClick()"></app-sub>
</div>
In your app root component typescript:
private functionHasCalled:boolean = false;
private dataInput:string;
onClick(){
//And you can initialize the input property also if you need
this.dataInput = 'asfsdfasdf';
this.functionHasCalled = true;
}
onSubComponentClick(){
}
Providing data for #Input is very easy. You have named your component app-sub and it has a #Input property named data. Providing this data can be done by doing this:
<app-sub [data]="whateverdatayouwant"></app-sub>
In my parent component, I want to create a child component with a unique ID associated with it, and I want to pass that unique ID into the child component, so the child component can put that ID on its template.
Parent template:
<ckeditor [ckEditorInstanceID]="someUniqueID"> </ckeditor>
Here is the child component:
import { Component, Input } from '#angular/core'
var loadScript = require('scriptjs');
declare var CKEDITOR;
#Component({
selector: 'ckeditor',
template: `<div [id]="ckEditorInstanceID">This will be my editor</div>`
})
export class CKEditor {
#Input() ckEditorInstanceID: string;
constructor() {
console.log(this.ckEditorInstanceID)
}
ngOnInit() {
}
ngAfterViewInit() {
loadScript('//cdn.ckeditor.com/4.5.11/standard/ckeditor.js', function() {
CKEDITOR.replace(this.ckEditorInstanceID);
console.info('CKEditor loaded async')
});
}
}
What am I missing? I can't seem to get the child component to receive the value of "someUniqueID". it is always undefined.
UPDATE: I was able to get the child component to receive the value "someUniqueID. Code udpated above. However, I cannot reference the #Input property by calling this.ckEditorInstanceID because this is undefined.
How do I reference the property I brought in via #Input?
Don't name inputs id. That's conflicting with the id attribute of the HTMLElement.
The trick was to use an arrow function like #David Bulte mentioned.
loadScript('//cdn.ckeditor.com/4.5.11/standard/ckeditor.js', () => {
CKEDITOR.replace(this.ckEditorInstanceID);
console.info('CKEditor loaded async')
});
For some reason the arrow function can access this.ckEditorInstanceID, but a regular function() {} cannot access this.ckEditorInstanceID. I don't know why, maybe someone can enlighten me to the reasoning for this.
In addition, I had to change my markup like this:
<ckeditor [ckEditorInstanceID]="'editor1'"> </ckeditor>
<ckeditor [ckEditorInstanceID]="'editor2'"> </ckeditor>
And set the #Input property to the name inside the [] which is ckEditorInstanceID , and also the template source should be the property name ckEditorInstanceID, like [id]="ckEditorInstanceID" .
Full working child component that receives the ID from the parent html selector:
import { Component, Input } from '#angular/core'
var loadScript = require('scriptjs');
declare var CKEDITOR;
#Component({
selector: 'ckeditor',
template: `<div [id]="ckEditorInstanceID">This will be my editor</div>`
})
export class CKEditor {
#Input() ckEditorInstanceID: string;
constructor() {}
ngAfterViewInit() {
loadScript('//cdn.ckeditor.com/4.5.11/standard/ckeditor.js', () => {
CKEDITOR.replace(this.ckEditorInstanceID);
console.info('CKEditor loaded async')
});
}
}
I'm trying to migrate my current Angular.js project to Aurelia.js.
I'm trying to do something like that:
report.js
export class Report {
list = [];
//TODO
listChanged(newList, oldList){
enter code here
}
}
report.html
<template>
<require from="component"></require>
<component list.bind="list"></component>
</template>
So the question is: how to detect when is the list changed?
In Angular.js I can do
$scope.$watchCollection('list', (newVal, oldVal)=>{ my code });
Maybe Aurelia have something similar?
For #bindable fields the listChanged(newValue, oldValue) would be called whenever the list value is updated. Please take a look Aurelia docs
#customAttribute('if')
#templateController
export class If {
constructor(viewFactory, viewSlot){
//
}
valueChanged(newValue, oldValue){
//
}
}
You can also use ObserveLocator as described in Aurelia author's blogpost here:
import {ObserverLocator} from 'aurelia-binding'; // or 'aurelia-framework'
#inject(ObserverLocator)
class Foo {
constructor(observerLocator) {
// the property we'll observe:
this.bar = 'baz';
// subscribe to the "bar" property's changes:
var subscription = this.observerLocator
.getObserver(this, 'bar')
.subscribe(this.onChange);
}
onChange(newValue, oldValue) {
alert(`bar changed from ${oldValue} to ${newValue}`);
}
}
UPD
As mentioned in this question by Jeremy Danyow:
The ObserverLocator is Aurelia's internal "bare metal" API. There's now a public API for the binding engine that could be used:
import {BindingEngine} from 'aurelia-binding'; // or from 'aurelia-framework'
#inject(BindingEngine)
export class ViewModel {
constructor(bindingEngine) {
this.obj = { foo: 'bar' };
// subscribe
let subscription = bindingEngine.propertyObserver(this.obj, 'foo')
.subscribe((newValue, oldValue) => console.log(newValue));
// unsubscribe
subscription.dispose();
}
}
Best regards, Alexander
Your original code will work with one small tweak:
report.js
import {bindable} from 'aurelia-framework'; // or 'aurelia-binding'
export class Report {
#bindable list; // decorate the list property with "bindable"
// Aurelia will call this automatically
listChanged(newList, oldList){
enter code here
}
}
report.html
<template>
<require from="component"></require>
<component list.bind="list"></component>
</template>
Aurelia has a convention that will look for a [propertyName]Changed method on your viewmodel and call it automatically. This convention is used with all properties decorated with #bindable. More info here
It seems better solution for current case is CustomeEvent
So complete solution would look like that
report.html
<template>
<require from="component"></require>
<component list.bind="list" change.trigger="listChanged($event)"></component>
</template>
component.js
#inject(Element)
export class ComponentCustomElement {
#bindable list = [];
//TODO invoke when you change the list
listArrayChanged() {
let e = new CustomEvent('change', {
detail: this.lis
});
this.element.dispatchEvent(e);
}
}
You have to change component element, add some trigger function that sand you change event. I suppose that component knows when list changed.