How does one reliably find out if a given Component is actually present in the DOM?
Until now I used Component.getUI().isPresent() which is supposed to able to determine if the Component is attached to a UI.
It also might be that I ran into an edge case as the Components in question are encapsulated in a ComponentRenderer which is managed by a Grid.
I need to access these Componets via JavaScript like this:
void setValue(Component comp, Value value){
Runnable callJs = comp.getUI().ifPresent(ui -> ui.getPage().executeJs("someCall($0)", value));
if(comp.isAttached()){
// execute it right away
callJs.run();
} else {
// execute onAttach
comp.addAttachListener(evt -> callJs.run());
}
}
After some digging I stumbled across the StateNode API:
comp.getElement().getNode().isAttached()
Related
I am trying to use Unity3D's UIToolkit with Visual Scripting Graph...
Typically, I can Query for the proper element, and then add the necessary function to the callback... However, as I am new to Visual Scripting, I am not sure how to get to this point. I've got the 'clickable' object of the button isolated, but I'm not sure how to assign the follow-up execution in the graph.
Usually in the code, I would do something like
clickable.clicked += ExampleFunction();
What I am messing up in the graph, is how to access the '.clicked' part. I can get the proper button element, and I can isolate it's clickable property, but I can't figure out how to assign some kind of functionality to react to the button getting clicked.
I could use a custom node to make this work, but I am looking for a way to do this with built-in nodes, if possible.
Alright... I had to write a custom node, but I figured this out. Here is the graph for the solution.
You have to grab the UIDocument from whichever GameObject it is attached to... You then need to get the Root Visual Element, do NOT clone or instantiate it. You then need to Query for the desired button, using the name you gave it in the UI Builder. It is easier if you use the U Query Extensions nodes... After that, I just made a custom node to subscribe the functionality. I am not familiar of any nodes that do this.
Here is the 'Subscribe Start Result' node code:
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UIElements;
public class SubscribeStartResult : Unit
{
[DoNotSerialize]
[PortLabelHidden]
public ControlInput inputTrigger;
[DoNotSerialize]
[PortLabelHidden]
public ValueInput element;
protected override void Definition()
{
element = ValueInput<Button>("element");
inputTrigger = ControlInput("inputTrigger", (flow) =>
{
flow.GetValue<Button>(element).clicked += () =>
{
Debug.Log("Button clicked");
};
return null;
});
}
}
With this setup, clicking the 'Start Button' in play-mode will log "Button clicked" in the Console.
The 'return null;' line is an artifact of the lambda. It is required to continue the control flow in the event this node has a follow-up... Otherwise, this combination of nodes and code allow you to assign callbacks for the UI Builder elements, using the Visual Scripting Graph.
The repro is a small example based on the maui template.
I created a button called MyButtonView and changed the MainPage to consume that control.
The button is created and shows correctly on the page, but when I try to create just the control as in
var b = new MyButtonView(); the handler is not created and I cant figure out how to get this created.
Notice in the source I have implemented the clicked event to show how the handler is not created. I am sure I am missing something but could someone lead me in the right direction?
Github repro
So it seems that if the control once created has a null handler, you will need to call MyButtonView.ToHandler(mauiContext); sounds simple, but getting the mauiContext is a bit of a pain.
The only way i was able to do this was to do the following in the MauiProgram.cs. This works for windows, have yet to try it with iOS
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<DtNavigationView, DtNavigationViewHandler>();
handlers.AddHandler<DtWindowTabView, DtWindowTabViewHandler>();
handlers.AddHandler<DtWindowTabItem, DtWindowTabItemHandler>();
});
builder.UseMauiEmbedding<Application>();
var mauiapp = builder.Build();
mauiContext = new MauiContext(mauiapp.Services);
return mauiapp;
Now you can use the static context to get the object to a handler, by using
MyButtonView.ToHandler(MauiProgram.mauiContext);
Dont think this is the best way to do this but its all i can come up with for now.
Update - this is not the answer to the issue. Storing the MauiContext at this point will result in other issues such as not being able to have the base navigation framework setup.
So the only work around i have found so far that will work for me is to capture the MauiContext was to save it off in the handler when
public override void SetMauiContext(IMauiContext mauiContext) { DtMauiContext.mauiContext = mauiContext; base.SetMauiContext(mauiContext); }
The DtMauiContext is a static i can use it in the view level.
In the Maui source they have Application.Current.FindMauiContext(), it would have been so easy of they just exposed this.
I need to show homeCard() and after I need to show settingsCard() automatically. Since I coudn't find a right method in app-script documentation I need some help for do this task.
Here I provided the code
function nevigateToUserSelectionPage(e) {
var navigation = CardService.newNavigation();
var builder = CardService.newActionResponseBuilder();
var userSelectionCardNavigation = navigation.pushCard(settingsCard());
return builder.setNavigation(userSelectionCardNavigation).build();
}
function homeCard() {
builder = CardService.newCardBuilder();
section = CardService.newCardSection();
let participantsText = CardService.newTextParagraph()
.setText("<u>Home card here</u>");
let blink = CardService
.newImage()
.setImageUrl('https://res.cloudinary.com/deez2bddk/image/upload/v1646709349/icons8-dots-loading_x9q7jv.gif');
section.addWidget(blink);
section.addWidget(participantsText);
section.addWidget(AddSplah);
builder.addSection(section);
console.log('home card triggered!!!');
return builder.build();
}
function settingsCard() {
//const myTimeout = setTimeout(5000);
Utilities.sleep(10000);
builder = CardService.newCardBuilder();
section = CardService.newCardSection();
console.log('Settings card triggered!!!');
let participantsText = CardService.newTextParagraph()
.setText("<u>This is Settings Page....</u>");
section.addWidget(participantsText);
section.addWidget(getAuthenticationStepperImage());
builder.addSection(section);
return builder.build();
}
in code.gs file
function mainController() {
return homeCard();
}
Above code blocks I need to execute homeCard() function and then , settingsCard() but I can`t find a proper solution in workspace add-on creation documentation provided by google.
After doing some research, I think the CardService does not provide a method for non-interactive updates.
You can update the view based on user click interaction, as you can see in the Cats Quickstart. When the user clicks the cat image changes the image updates due the URL has a new parameter via new Date().getTime().
Apart from this, you have the triggers provided by Google, such as: homepageTrigger for common use case or onItemsSelectedTrigger specifically for Drive. You can review the full list here.
In summary: I think that what are you trying to achieve actually is not currently feasible within CardService.
If you wish Google adds some kind of time driven trigger to Google Workspace Add-ons, request it via this form.
Remember that in the actual state, HTML/CSS is not allowed, maybe this would be another possible path for your Feature Request.
I have a problem with some dynamically generated forms and passing values to them. I feel like someone must have solved this, or I’m missing something obvious, but I can't find any mention of it.
So for example, I have three components, a parent, a child, and then a child of that child. For names, I’ll go with, formComponent, questionComponent, textBoxComponent. Both of the children are using changeDetection.OnPush.
So form component passes some values down to questionComponent through the inputs, and some are using the async pipe to subscribe to their respective values in the store.
QuestionComponent dynamically creates different components, then places them on the page if they match (so many types of components, but each questionComponent only handles on one component.
some code:
#Input() normalValue
#Input() asyncPipedValue
#ViewChild('questionRef', {read: ViewContainerRef}) public questionRef: any;
private textBoxComponent: ComponentFactory<TextBoxComponent>;
ngOnInit() {
let component =
this.questionRef.createComponent(this.checkboxComponent);
component.instance.normalValue = this.normalValue;
component.instance. asyncPipedValue = this. asyncPipedValue;
}
This works fine for all instances of normalValues, but not for asyncValues. I can confirm in questionComponent’s ngOnChanges that the value is being updated, but that value is not passed to textBoxComponent.
What I basically need is the async pipe, but not for templates. I’ve tried multiple solutions to different ways to pass asyncValues, I’ve tried detecting when asyncPipeValue changes, and triggering changeDetectionRef.markForChanges() on the textBoxComponent, but that only works when I change the changeDetectionStrategy to normal, which kinda defeats the performance gains I get from using ngrx.
This seems like too big of an oversight to not already have a solution, so I’m assuming it’s just me not thinking of something. Any thoughts?
I do something similar, whereby I have forms populated from data coming from my Ngrx Store. My forms aren't dynamic so I'm not 100% sure if this will also work for you.
Define your input with just a setter, then call patchValue(), or setValue() on your form/ form control. Your root component stays the same, passing the data into your next component with the async pipe.
#Input() set asyncPipedValue(data) {
if (data) {
this.textBoxComponent.patchValue(data);
}
}
patchValue() is on the AbstractControl class. If you don't have access to that from your question component, your TextBoxComponent could expose a similar method, that can be called from your QuestionComponent, with the implementation performing the update of the control.
One thing to watch out for though, if you're also subscribing to valueChanges on your form/control, you may want to set the second parameter so the valueChanges event doesn't fire immediately.
this.textBoxComponent.patchValue(data, { emitEvent: false });
or
this.textBoxComponent.setValue(...same as above);
Then in your TextBoxComponent
this.myTextBox.valueChanges
.debounceTime(a couple of seconds maybe)
.distinctUntilChanged()
.map(changes => {
this.store.dispatch(changes);
})
.subscribe();
This approach is working pretty well, and removes the need to have save/update buttons everywhere.
I believe I have figured out a solution (with some help from the gitter.com/angular channel).
Since the values are coming in to the questionComponent can change, and trigger it's ngOnChanges to fire, whenever there is an event in ngOnChanges, it needs to parse through the event, and bind and changes to the dynamic child component.
ngOnChanges(event) {
if (this.component) {
_.forEach(event, (value, key) => {
if (value && value.currentValue) {
this.component.instance[key] = value.currentValue;
}
});
}
}
This is all in questionComponent, it resets the components instance variables if they have changed. The biggest problem with this so far, is that the child's ngOnChanges doesn't fire, so this isn't a full solution. I'll continue to dig into it.
Here are my thoughts on the question, taking into account limited code snippet.
First, provided example doesn't seem to have anything to do with ngrx. In this case, it is expected that ngOnInit runs only once and at that time this.asyncPipedValue value is undefined. Consequently, if changeDetection of this.checkboxComponent is ChangeDetection.OnPush the value won't get updated. I recommend reading one excellent article about change detection and passing async inputs. That article also contains other not less great resources on change detection. In addition, it seems that the same inputs are passed twice through the component tree which is not a good solution from my point of view.
Second, another approach would be to use ngrx and then you don't need to pass any async inputs at all. Especially, this way is good if two components do not have the parent-child relationship in the component tree. In this case, one component dispatches action to put data to Store and another component subscribes to that data from Store.
export class DataDispatcherCmp {
constructor(private store: Store<ApplicationState>) {
}
onNewData(data: SomeData) {
this.store.dispatch(new SetNewDataAction(data));
}
}
export class DataConsumerCmp implements OnInit {
newData$: Observable<SomeData>;
constructor(private store: Store<ApplicationState>) {
}
ngOnInit() {
this.newData$ = this.store.select('someData');
}
}
Hope this helps or gives some clues at least.
I've been looking for a script that combines the autoGrowInput with the JEditable but found none.
Use https://github.com/MartinF/jQuery.Autosize.Input initialized automatically via jEditable's event data:
jQuery(element).editable(save_fn, {
data: function(value,settings} {
var target = event.target;
window.setTimeout(function(){
jQuery(target).find('input').autosizeInput();
});
return value;
}
});
It's worth noting that this event (data) fires before the input element is actually created, hence the use of the timeout. There doesn't seem to be an event available at the present time for after the input has been created.
Actually I have created a plugin that does exactly that. You can check the demo and the documentation. I tried to make it very intuitive. It has ajax capabilities, using the RESTful philosophy. If you liked the animation effect on the autoGrowInput, it will be really easy to add it to the plugin just by changing the css file, using the transition property.
If I get people to like it, I may be able to improve and add more features to it. Hope it helps.