-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Advice for dealing with ansync setup in parent components? #57
Comments
@jagthedrummer this is an interesting point you bring up. ember-composability-tools could be promise aware. But for your specific use case, it seems to me that neither I'm looking at this example: https://wavesurfer.xyz/examples/?regions.js Can you share the code for the |
Thanks for the response, @miguelcobain! I think you're right about neither of those methods returning a promise. Do you think that's the root of the problem? Like, would you expect Here are the relevant bits (I hope).
export default class BouncePlayerWavesurferDisplayComponent extends Component {
componentsToYield = [
{ name: 'timeline', as: 'timeline', component: BouncePlayerWavesurferTimelineComponent },
{ name: 'regions', as: 'regions', component: BouncePlayerWavesurferRegionsComponent },
];
@action
mergeComponents(obj) {
if (!this.mergedComponents) {
this.mergedComponents = obj;
} else {
Object.assign(this.mergedComponents, obj);
}
}
@action
async didInsertParent(element) {
this._element = element;
await this._buildWavesurfer.perform();
this.setWavesurferTime();
}
@action
willDestroyParent(element) {
this._destroyWavesurfer.perform()
}
_buildWavesurfer = task(async () => {
this.wavesurfer = await WaveSurfer.create({
container: this._element,
// ... a bunch of other (probably) unrelated options
})
})
_destroyWavesurfer = task(async () => {
if(this.wavesurfer){
this.wavesurfer.empty();
this.wavesurfer.unAll();
this.wavesurfer.destroy();
this.wavesurfer = null;
}
})
}
<Root
@didInsertParent={{this.didInsertParent}}
@willDestroyParent={{this.willDestroyParent}}
{{did-update this.setWavesurferTime @currentTime}}
...attributes
as |Node|
>
{{#let (seshy-hash) as |components|}}
{{#each this.componentsToYield as |c|}}
{{seshy-assign-to
components
key=(seshy-or c.as c.name)
value=(component (ensure-safe-component (seshy-or c.component c.name)) parent=this node=Node wavesurfer=this.wavesurfer)
onChange=this.mergeComponents
}}
{{/each}}
{{yield this.mergedComponents}}
{{/let}}
</Root>
export default class BouncePlayerWavesurferRegionsComponent extends Component {
@tracked regions = null;
componentsToYield = [
{ name: 'marker', as: 'marker', component: BouncePlayerWavesurferMarkerComponent }
];
@action
mergeComponents(obj) {
if (!this.mergedComponents) {
this.mergedComponents = obj;
} else {
Object.assign(this.mergedComponents, obj);
}
}
@action
async didInsertParent(element) {
this._element = element;
await this._buildRegions.perform();
}
@action
willDestroyParent(element) {
this._destroyRegions.perform()
}
_buildRegions = task(async () => {
this.regions = await RegionsPlugin.create({});
await this.args.wavesurfer.registerPlugin(this.regions);
})
_destroyRegions = task(async () => {
if(this.regions){
this.regions.destroy();
this.regions= null;
}
})
}
<@node
@didInsertParent={{this.didInsertParent}}
@willDestroyParent={{this.willDestroyParent}}
@tagName="div"
...attributes
as |Node|
>
{{#let (seshy-hash) as |components|}}
{{#each this.componentsToYield as |c|}}
{{seshy-assign-to
components
key=(seshy-or c.as c.name)
value=(component (ensure-safe-component (seshy-or c.component c.name)) parent=this node=Node regions=this.regions)
onChange=this.mergeComponents
}}
{{/each}}
{{yield this.mergedComponents}}
{{/let}}
</@node>
export default class BouncePlayerWavesurferMarkerComponent extends Component {
@action
async didInsertParent(element) {
this._element = element;
await this._buildMarker.perform();
}
@action
willDestroyParent(element) {
this._destroyMarker.perform()
}
@action
updateMarker(){
this.displayMarker.setOptions({
start: this.args.start,
end: this.args.end
})
}
_buildMarker = task(async () => {
let regions = this.args.regions;
// TODO: How can we synchronize things so that we don't have
// to do this ugliness? Sometimes we fall into this block and
// sometimes we don't.
if(!regions){
console.error('the regions are missing, try again later');
later(() => {
this._buildMarker.perform();
});
return;
}
this.displayMarker = await regions.addRegion({
start: this.args.start,
content: this.regionContent,
color: 'rgba(0,0,255,0.5)',
end: this.args.end || this.args.start + 0.20
})
})
_destroyMarker = task(async () => {
if(this.displayMarker){
this.displayMarker.remove();
this.displayMarker = null;
}
})
}
<@node
@didInsertParent={{this.didInsertParent}}
@willDestroyParent={{this.willDestroyParent}}
...attributes
as |Node|
>
<div class="hidden" {{did-update this.updateMarker @start @end}}>
Hidden div for triggering updates since
Node doesn't actually render anything.
</div>
{{yield}}
</@node> |
Firstly, make sure you need all the complexity for yielding an arbitrary amount of components. Secondly, I think think them not returning a promise is a problem. On the contrary, it's a good thing. The fact that you were initializing wavesurfer in a task might be cause some timing issues. I don't know. // bounce-player/wavesurfer.js
// ...
@action
didInsertParent(element) {
this.wavesurfer = await WaveSurfer.create({
container: element,
// ... a bunch of other (probably) unrelated options
});
}
@action
willDestroyParent(element) {
// destroy this.wavesurfer using whatever methods it has for that purpose
} {{! bounce-player/wavesurfer.hbs }}
<Root
@didInsertParent={{this.didInsertParent}}
@willDestroyParent={{this.willDestroyParent}}
...attributes
as |Node|
>
{{yield (hash
timeline=(component "bounce-player/wave-surfer/regions" parent=this node=Node)
regions=(component "bounce-player/wave-surfer/regions" parent=this node=Node)
))}}
</Root> // wavesurfer/regions.js
@action
didInsertParent(element) {
this.regions = RegionsPlugin.create({});
this.args.parent.wavesurfer.registerPlugin(this.regions);
} |
Thanks so much for the recommendations @miguelcobain! I had the feeling that cribbing I'm about to duck out for the afternoon but will give this a shot later tonight and will report back. Thanks again for the help, and for this excellent add-on! I think this is going to be a much better way of organizing all of this than the "one giant component managing everything directly in JS" approach that I'd taken previously. |
Just spent some time with this and the unfortunately the suggestions didn't solve the problem. The simplifications around But moving the setup code out of a task and directly into In the interest of trying to untangle whether I have a Here's a parent component that uses a
<Root
...attributes
@didInsertParent={{this.didInsertParent}}
as |Node|
>
<h3>The parent</h3>
{{yield (component "Test/Child" node=Node parent=this)}}
</Root>
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { timeout } from 'ember-concurrency';
import { tracked } from '@glimmer/tracking';
export default class TestParentComponent extends Component {
@tracked isReady = false;
@action
async didInsertParent(element) {
console.log('Parent: didInsertParent')
await timeout(10); // Simulate some setup that happens async
this.isReady = true;
console.log('Parent: isReady')
}
}
<@node
@didInsertParent={{this.didInsertParent}}
>
<h3>The child</h3>
</@node>
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class TestChildComponent extends Component {
@action
didInsertParent(element) {
console.log('Child: didInsertParent', this.args.id)
if(this.args.parent.isReady){
console.log('the parent is ready = ', this.args.parent.isReady);
}else{
console.error('the parent is NOT ready = ', this.args.parent.isReady);
}
}
}
<Test::Parent as |Test::Child|>
<Test::Child/>
</Test::Parent> |
I'm trying to work around the problem by having the parent notify it's children when it's finally ready, but In @action
async didInsertParent(element) {
console.log('Parent: didInsertParent')
await timeout(1000);
this.isReady = true;
for(let child of this.children){
child.parentIsReady();
}
} When this runs it throws an error:
If I do Even in actions fired after the components are setup and rendered (like on a button click or something) |
Seems like maybe the problem with I'm still pretty confused on why the docs say to extend (Is it possible that the async problems are due to not extending |
I'm able get things to kind of work if I manually track a in @tracked customChildren = A([]);
registerChild(child){
this.customChildren.pushObject(child);
}
@action
async didInsertParent(){
console.log('Parent: didInsertParent', this.args.id)
await timeout(1000);
this.isReady = true;
console.log('Parent isReady', this.customChildren, this);
for(let child of this.customChildren){
child.parentIsReady();
}
} in parentIsReady(){
console.log('Child: parent is ready')
}
didInsertParentTask(){
console.log('Child: didInsertParent')
if(this.args.parent.isReady){
console.log('the parent is ready = ', this.args.parent.isReady);
this.parentIsReady();
}else{
console.error('the parent is NOT ready = ', this.args.parent.isReady);
this.args.parent.registerChild(this);
}
} |
OK, I figured out how to have my component BE a
<div
{{!--
These two actions are inherited from Root, but we need to call them
manually since we're not using the Root template.
--}}
{{did-insert this.didInsertNode}}
{{will-destroy this.willDestroyNode}}
...attributes
>
{{yield (component "Test/Child" parent=this)}}
</div>
import { Root } from 'ember-composability-tools';
export default class TestParentComponent extends Root {
@tracked isReady = false;
// this doesn't need to be an action since it's called directly by Root
async didInsertParent(element) {
console.log('Parent: didInsertParent')
await timeout(1000);
this.isReady = true;
console.log('Parent: finally ready')
for(let child of this.children){
child.parentIsReady();
}
}
}
<div>
<h3>The child</h3>
</div>
import { Node } from 'ember-composability-tools';
export default class TestChildComponent extends Node {
parentIsReady(){
console.log('Child: parent is ready')
}
// this doesn't need to be an action since it's called directly by Node
didInsertParent(element) {
console.log('Child: didInsertParent', this.args.id)
if(this.args.parent.isReady){
console.log('the parent is ready = ', this.args.parent.isReady);
this.parentIsReady();
}else{
console.error('the parent is NOT ready = ', this.args.parent.isReady);
}
}
} |
I'm curious if there's any general advice for how to deal with async methods when using this addon. The setup process of my parent component takes some time, and then the
didInsertParent
of the child component is called before the parent component is fully ready. Is there a standard way of synchronizing everything?I think that covers the general question, but just in case more details would be helpful here are some more.
This is the general structure that I'm going for.
In the
Regions
component thedidInsertParent
method looks like this:Sometimes this works great, but sometimes it fails with an error saying
Uncaught TypeError: Cannot read properties of null (reading 'registerPlugin')
due to thewavesurfer
object that is passed in from the parent not being full initialized yet.Is there a standard way of dealing with this sort of situation?
The text was updated successfully, but these errors were encountered: