Is there any analogues functions $apply or $digest in Aurelia ?
How immediately call changes bindingValue?
My case:
I have tree(each node contain list of item, screen 1). I have parent component(names: tree), and node component(names:node).
Each node have a toggle button. When i'm toggle my items parent component should know how changes content height.
toggleNode(event) {
for (var i = 0, length = this.current.children.length; i < length; i++) {
this.current.children[i].visible = !this.current.children[i].visible;
}
//some code
this.evAggregator.publish("toggle-agents", event);
}
View:
<ol show.bind="current.visible">//Some markup</ol>
My parent component catch this event and check content size:
#autoinject
export class Agents {
constructor(private evAggregator: EventAggregator) {
this.toggleAgentsSubscriber = this.evAggregator.subscribe("toggle- agents", (e) => {
//some code for recalculate content height
});
}
}
Now code execute that:
1) this.current.children[i].visible = false(when node collapse)
2) Fire my event "toggle-agents"
3) Subscriber catch my event (recalculate height)
4) In depth at Aurelia ObserverLocator update my (visible property) in DOM and height changes.
I need:
1) this.current.children[i].visible = false(when node collapse)
2) In depth at Aurelia ObserverLocator update my (visible property) in DOM and height changes.
3) Fire my custom event.
4) Subscriber catch my event and recalculate height when height content actually changes.
In Aurelia, changes are immediately applied for the most part. Aurelia only uses dirty checking for computed properties (properties with a getter function). If you wanted to manually invoke the dirty-checking, you could do something like this:
import {DirtyChecker} from 'aurelia-binding';
import {inject} from 'aurelia-dependency-injection';
#inject(DirtyChecker)
export class Foo {
constructor(dirtyChecker) {
dirtyChecker.check(); // force an application-wide dirty check
}
}
In practice, this sort of thing is never needed in an Aurelia app. If you have specific bindings that you want to force to update you can use the signal binding behavior:
<template>
<label>${foo & signal:'my signal name'}</label>
</template>
import {BindingSignaler} from 'aurelia-templating-resources';
import {inject} from 'aurelia-dependency-injection';
#inject(BindingSignaler)
export class Foo {
constructor(signaler) {
signaler.signal('my signal name'); // evaluate any bindings with 'my signal name'
}
}
Try to stay away from these two techniques if you can. If you're using this for something other than time-based bindings (binding to something that uses Date.now() or new Date()) or internationalization locale-changed events... you might not be doing it the "Aurelia way".
Related
I have one parent and 2 child components like below
Parent-component.html
<child-component-1 [id]="id"></child-component-1>
<child-component-2></child-component-2>
child-component-1 has ngb-carousel component to show warnings and alerts to user.And this component is being created due to id and its result comes from API like below
child-component-1.ts
alertFound;
this.service.checkAlert(id).subscribe((result:any)=>{
if(result.alertFound){
this.alertFound=true;
}
})
child-component-1.html
<ngb-carousel #carousel *ngIf="alertFound" class="carousel-alert" [interval]="false" [wrap]="false"
[showNavigationIndicators]="false">
........................(Things go on here after result comes)
</ngb-carousel>
In child-component-2,I need to get the height of this ngb-alert to make dynamic height calculation on the screen.If it exists,I need to subtract it from window.innerheight.
child-component-2.html
<div [style.height.px]="actualHeight"></div>
child-component-2.ts
this.actualHeight = window.innerHeight - 269;
Interestingly on child-component-1 when I tried to track it like below
#ViewChild('carousel', {read: ElementRef, static:false}) elementView: ElementRef;
ngAfterViewInit(){
console.log(this.elementView.nativeElement.offSetHeight);
//throws height of created carousel first,then if next id doesnt have alert still shows me the height which previously created
}
it shows previously created carousel height even though result is false and carousel couldn't be created at that moment.I guess this happening because result sometimes arrives very late,sometimes fast.To listen detections on viewchild I found something like below which works exactly as I wanted and solved emitting non-created alert height problem
#ViewChildren('carousel', {read: ViewContainerRef}) viewContainerRefs;
ngAfterViewInit() {
this.viewContainerRefs.changes.subscribe(item => {
if (item) {
console.log(item._results[0]._lContainer[0][0].offsetHeight);
}
})
}
Now problem is sending this height to child-component-2 where I should calculate height dynamically.
I considered 2 options.First one is creating subject on a service,emitting height to it from child-component-1 and listening it on child-component-2 like below
service.ts
alertActive = new BehaviorSubject(0);
setAlertHeight(value:number){
this.alertActive.next(value)
}
child-component-1
ngAfterViewInit() {
this.viewContainerRefs.changes.subscribe(item => {
if (item) {
this.service.setAlertHeight(item._results[0]._lContainer[0][0].offsetHeight);
}
})
}
child-component-2
this.alertActive.subscribe(value=>{
if(value){
this.actualHeight-=value;
}
})
On the example above,it causes the problem to bring height,even though alert not created.I logged subscribes on console from child-2 and child-1 and noticed that child-2 prints to console even though child1 didnt emit anything to it.
So I considered another option to send height from child-1 to parent via #Output and then via Input to
child2 like below
child-1
#Output() transferHeight: EventEmitter<any> = new EventEmitter();
this.viewContainerRefs.changes.subscribe(item => {
if (item) {
this.transferHeight.emit(item._results[0]._lContainer[0][0].offsetHeight);
}
})
parent
<child-component-1 (transferHeight)="transferedHeight($event)" [id]="id"></child-component-1>
<child-component-2 [transferredHeight]="transferredHeight"></child-component-2>
alertHeight;
transferedHeight(comingHeight){
this.alertHeight=comingHeight;
}
child-2
#Input() transferredHeight;
ngOnInit(){
this.actualHeight-=this.transferredHeight;
}
this one really handles the problem that I mentioned previously.But if carousel created after child-2 created due to late network resut ,it returns undefined.Therefore I tried to use ngOnChanges to listen changes on the Input variable like below
ngOnChanges(changes: SimpleChanges) {
console.log(changes.transferredHeight.currentValue)
}
this console.log doesnt print anything although I implemented onChanges.So Im waiting for your help.If there's a way to listen viewContainerRefs.changes of child-1 from child-2 would be the best case
The reason this attempt:
alertActive = new BehaviorSubject(0);
setAlertHeight(value:number){
this.alertActive.next(value)
}
triggered the height to be set even when the alert was not active was because you used a BehaviorSubject rather than just a Subject. A BehaviorSubject provides an initial value (0 in your case), so immediately upon subscription the subscriber will receive that initial value (or the latest emitted value).
So you were on the right track, you just needed to use a regular Subject instead because then subscribers won't receive any values until the Subject emits.
So your service could look like this:
private _alertActive = new Subject<number>();
get alertActive(): Observable<number> {
return this._alertActive.asObservable();
}
setAlertHeight(height: number) {
this._alertActive.next(height);
}
(Note that the Subject is a private member but is exposed via a getter as an Observable. In general, subscribers shouldn't have access to the raw Subject unless they'll also be emitting values from it.)
And your child-2 component would subscribe to it like so:
this.service.alertActive.subscribe((height: number) => {
this.actualHeight -= height;
});
You were also on the right track using ViewChildren and subscribing to its changes, but it might be simpler if you reference the ElementRefs rather than the ViewContainerRefs. That would look like this:
import { NgbCarousel } from "#ng-bootstrap/ng-bootstrap";
#ViewChildren(NgbCarousel, { read: ElementRef }) carousels: QueryList<ElementRef>;
ngAfterViewInit() {
this.carousels.changes.subscribe((els: QueryList<ElementRef>) => {
const height = els.first ? els.first.nativeElement.offsetHeight : 0;
this.service.setAlertHeight(height);
});
}
Here's a StackBlitz showing it working with this approach.
There are a bunch of similar questions on so, but I can't see one that matches my conundrum.
I have a react component (a radial knob control - kinda like a slider).
I want to achieve two outcomes:
Twiddle the knob and pass the knob value up to the parent for further actions.
Receive a target knob value from the parent and update the knob accordingly.
All without going into an endless loop!
I have pulled my hair out - but have a working solution that seems to violate react principles.
I have knob.js as a react component that wraps around the third party knob component and I have app.js as the parent.
In knob.js, we have:
export default class MyKnob extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
size: props.size || 100,
radius: (props.value/2).toString(),
fontSize: (props.size * .2)
}
if (props.value){
console.log("setting value prop", props.value)
this.state.value = props.value
} else {
this.state.value = 25 // any old default value
}
}
To handle updates from the parent (app.js) I have this in knob.js:
// this is required to allow changes at the parent to update the knob
componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value) {
this.setState({value: this.props.value})
}
console.log("updating knob from parent", value)
}
and then to pass changes in knob value back to the parent, I have:
handleOnChange = (e)=>{
//this.setState({value: e}) <--used to be required until line below inserted.
this.props.handleChangePan(e)
}
This also works but triggers a warning:
Cannot update a component (App) while rendering a different component (Knob)
render(){
return (
<Styles font-size={this.state.fontSize}>
<Knob size={this.state.size}
angleOffset={220}
angleRange={280}
steps={10}
min={0}
max={100}
value={this.state.value}
ref={this.ref}
onChange={value => this.handleOnChange(value)}
>
...
Now over to app.js:
function App() {
const [panLevel, setPanLevel] = useState(50);
// called by the child knob component. works -- but creates the warning
function handleChangePan(e){
setPanLevel(e)
}
// helper function for testing
function changePan(e){
if (panLevel + 10>100){
setPanLevel(0)
} else {
setPanLevel(panLevel+10)
}
}
return (
<div className="App">
....
<div className='mixer'>
<div key={1} className='vStrip'>
<Knob size={150} value={panLevel} handleChangePan = {(e) => handleChangePan(e)}/>
</div>
<button onClick={(e) => changePan(e)}>CLICK ME TO INCREMENT BY 10</button>
...
</div>
So - it works -- but I am violating react principles -- I haven't found another way to keep the external "knob value" and the internal "knob value" in sync.
Just to mess with my head further, if I remove the bubbling to parent in 'handleOnChange' - which presumably then triggers a change in prop-->state cascading back down - I not only have a lack of sync with the parent -- but I also need to reinstate the setState below, in order to get the knob to work via twiddling (mouse etc.._)! This creates another warning:
Update during an existing state transition...
So stuck. Advice requested and gratefully received. Apols for the long post.
handleOnChange = (e)=>{
//this.setState({value: e})
**this.props.handleChangePan(e)**
}
It has been suggested on another post, that one should wrap the setState into a useEffect - but I can't figure out how to do that - let alone whether it's the right approach.
The error message will be displayed if parent (App) states are set while rendering children (Knob).
In your case, while App is rendering, Knob'sonChange() is triggered when loaded, which then calls this.handleOnChange() and then this.props.handleChangePan() having App'ssetPanLevel().
To fix using useEffect():
In knob.js, you can store panLevel as state first just like in App, instead of direct calling this.props.handleChangePan() to call App'ssetPanLevel().
Then, use useEffect(_=>props.handleChangePan(panLevel),[panLevel]) to call App'ssetPanLevel() via useEffect().
Your knob.js will look like this:
function Knob(props){
let [panLevel, setPanLevel] = useState(50);
useEffect(_=>{
props.handleChangePan(panLevel);
}, [panLevel]);
return *** Knob that does not call props.handleChangePan(), but call setPanLevel() instead ***;
}
setState() called inside useEffect() will be effective after the render is done.
In short, you cannot call parent'ssetState() outside useEffect() while in first rendering, or the error message will come up.
The content of my Vue app is fetched from Prismic (an API CMS). I have a rich text block, some parts of which are wrapped inside span tags with a specific class. I want to get those span nodes with Vue and add to them an event listener.
With JS, this code would work:
var selectedSpanElements = document.querySelectorAll('.className');
selectedSpanElements[0].style.color = "red"
But when I use this code in Vue, I can see that it works just a fraction of a second before Vue updates the DOM. I've tried using this code on mounted, beforeupdate, updated, ready hooks... Nothing has worked.
Update: Some hours later, I found that with the HTMLSerializer I can add HTML code to the span tag. But this is regular HTML, I cannot access to Vue methods.
#Bruja
I was able to find a solution using a closure. The folks at Prismic reminded/showed me.
Of note, per Phil Snow's comment above: If you are using Nuxt you won't have access to Vue's functionality and will have to go old-school JS.
Here is an example where you can pass in component-level props, data, methods, etc... to the prismic htmlSerializer:
<template>
<div>
<prismic-rich-text
:field="data"
:htmlSerializer="anotherHtmlSerializer((startNumber = list.start_number))"
/>
</div>
</template>
import prismicDOM from 'prismic-dom';
export default {
methods: {
anotherHtmlSerializer(startNumber = 1) {
const Elements = prismicDOM.RichText.Elements;
const that = this;
return function(type, element, content, children) {
// To add more elements and customizations use this as a reference:
// https://prismic.io/docs/vuejs/beyond-the-api/html-serializer
that.testMethod(startNumber);
switch (type) {
case Elements.oList:
return `<ol start=${startNumber}>${children.join('')}</ol>`;
}
// Return null to stick with the default behavior for everything else
return null;
};
},
testMethod(startNumber) {
console.log('test method here');
console.log(startNumber);
}
}
};
I believe you are on the right track looking into the HTML Serializer. If you want all your .specialClass <span> elements to trigger a click event that calls specialmethod() this should work for you:
import prismicDOM from 'prismic-dom';
const Elements = prismicDOM.RichText.Elements;
export default function (type, element, content, children) {
// I'm not 100% sure if element.className is correct, investigate with your devTools if it doesn't work
if (type === Elements.span && element.className === "specialClass") {
return `<span #click="specialMethod">${content}</span>`;
}
// Return null to stick with the default behavior for everything else
return null;
};
I have a component ResultPill with a tooltip (implemented via vuikit) for the main container. The tooltip text is calculated by a getter function tooltip (I use vue-property-decorator) so the relevant bits are:
<template>
<div class="pill"
v-vk-tooltip="{ title: tooltip, duration: 0, cls: 'some-custom-class uk-active' }"
ref="container"
>
..some content goes here..
</div>
</template>
<script lang="ts">
#Component({ props: ... })
export default class ResultPill extends Vue {
...
get tooltip (): string { ..calcing tooltip here.. }
isContainerSqueezed (): boolean {
const container = this.$refs.container as HTMLElement | undefined;
if(!container) return false;
return container.scrollWidth != container.clientWidth;
}
...
</script>
<style lang="stylus" scoped>
.pill
white-space pre
overflow hidden
text-overflow ellipsis
...
</style>
Now I'm trying to add some content to the tooltip when the component is squeezed by the container's width and hence the overflow styles are applied. Using console, I can roughly check this using $0.scrollWidth == $0.clientWidth (where $0 is the selected element), but when I start tooltip implementation with
get tooltip (): string {
if(this.isContainerSqueezed())
return 'aha!'
I find that for many instances of my component this.$refs.container is undefined so isContainerSqueezed doesn't help really. Do I have to somehow set unique ref per component instance? Are there other problems with this approach? How can I check whether the element is overflown?
PS to check if the non-uniqueness of refs may affect the case, I've tried to add to the class a random id property:
containerId = 'ref' + Math.random();
and use it like this:
:ref="containerId"
>
....
const container = this.$refs[this.containerId] as HTMLElement | undefined;
but it didn't help: still tooltip isn't altered.
And even better, there's the $el property which I can use instead of refs, but that still doesn't help. Looks like the cause is this:
An important note about the ref registration timing: because the refs themselves are created as a result of the render function, you cannot access them on the initial render - they don’t exist yet! $refs is also non-reactive, therefore you should not attempt to use it in templates for data-binding.
(presumably the same is applicable to $el) So I have to somehow recalc tooltip on mount. This question looks like what I need, but the answer is not applicable for my case.
So, like I've mentioned in one of the edits, docs warn that $refs shouldn't be used for initial rendering since they are not defined at that time. So, I've made tooltip a property instead of a getter and calcuate it in mounted:
export default class ResultPill extends Vue {
...
tooltip = '';
calcTooltip () {
// specific logic here is not important, the important bit is this.isContainerSqueezed()
// works correctly at this point
this.tooltip = !this.isContainerSqueezed() ? this.mainTooltip :
this.label + (this.mainTooltip ? '\n\n' + this.mainTooltip : '');
}
get mainTooltip (): string { ..previously used calculation.. }
...
mounted () {
this.calcTooltip()
}
}
I have a service where I'm listening to browser print events.
#Injectable()
export class ApplicationSession {
printStream$: Observable<boolean>;
constructor() {
this._setupPrintListener();
}
private _setupPrintListener(): void {
if (this._window.matchMedia) {
const beforePrintEvent = fromEvent(this._window, 'beforeprint')
.pipe(mapTo(true));
const afterPrintEvent = fromEvent(this._window, 'afterprint')
.pipe(mapTo(false));
this.printStream$ = merge(beforePrintEvent, afterPrintEvent).pipe(startWith(false));
}
}
}
Then, in my Component, I'm binding property to the printStream$ property of the service instance. As in,
export class ReferralComponent {
printRequested$: Observable<boolean>;
constructor(session: ApplicationSession) {
this.printRequested$ = session.printStream$;
//Observing values here
this.printRequested$.subscribe(console.log);
}
}
I use the component's printRequested$ property to create and destroy an angular component asynchronously.
<generic-angular-component *ngIf="printRequested$ | async"></generic-angular-component>
I have a child component within the ReferralComponent that has a button which triggers window.print() function. As in,
#Component({
template: `<button (click)="print()">Print</button>`
})
export class ChildComponent {
print() {
window.print();
}
}
My problem is, when I press Command/Ctrl + P from the keyboard, I see angular creating and destroying the <generic-angular-component>. However, when I trigger the window.print() via the child component button click, I can see the true/false values being passed on the stream. However, Angular doesn't seem to care about it at all. I don't see the component at all.
Here's a stackblitz reproduction.
You can either click on Print button and see that there's no Toggle Me line at the bottom left of the print preview page. Or, you can press Command/Ctrl + P and see that it (Toggle Me) is there at the bottom left.