How to detect svelte component from DOM? - dom

Currently making an google chrome extension to visualize svelte components, this would only be used only development mode. Currently I am grabbing all svelte components by using const svelteComponets = document.querySelectorAll(`[class^="svelte"]`); on my content scripts but it is grabbing every svelte element. What are some approaches to grab only the components?

Well you mostly can't get to the Svelte component from the DOM elements.
The reason, appart from Svelte won't give you / expose what's needed, is that there isn't a reliable link between components and elements.
A component can have no elements:
<slot />
Or "maybe no elements":
{#if false}<div />{/if}
It can also have multiple root elements:
<div> A </div>
<div> B </div>
<div> C </div>
By bending the cssHash compiler option a lot, you would probably be able to extract the component "name", maybe class name from the CSS scoping classes generated by Svelte. (Which, in turn could break CSS-only HMR updates with Vite, but that's another story.)
But from there, you won't be able to reliably get to the individual component instances... If we keep the component from the last example, once you've grabbed those 6 divs:
<div> A </div>
<div> B </div>
<div> C </div>
<div> A </div>
<div> B </div>
<div> C </div>
... how do you know where one component instance ends and where the other begins? Or even that there are two components?
I believe, the most reliable way to achieve what you want is probably to use internal Svelte APIs, including those that are used by the actual Svelte dev tools that you want to mimic. (Gotta love when private APIs are the "most reliable"!)
Necessary disclaimer: this only seems reasonable to do this in your case because it is a study subject, and because it's dev only. It would certainly not be wise to rely on this for something important. Private / internal APIs can change with any release without any notice.
If you go in the Svelte REPL and look at the generated JS after enabling the "dev" option, you'll see that the compiler adds some events that are provided for the dev tools.
By trials and experimentation, you can get a sense of how Svelte works, and what dev events are available. You'd also probably need to dig the sources of the compiler itself to understand what's happening with some functions... Being comfortable with a good debugger can help a lot!
For your intended usage, that is build a representation of the Svelte component tree, you'll need to know when a component instance is created, what is its parent component, and when it is destroyed. To add it to the tree, in the right place, and remove it when it goes away. With that you should be able to maintain a representation of the component tree for yourself.
You can know when a component is created with the "SvelteRegisterComponent" dev event (squared in red in the above screenshot). You can know the parent component of a component being instantiated by abusing { current_component } from 'svelte/internal'. And you can know when a component is destroyed by abusing the component's this.$$.on_destroy callbacks (which seems like the most fragile part of our plan).
Going into much more detail about how to proceed with this seems of bit out of scope for this question, but the following basic example should give you some ideas of how you can proceed. See it in action in this REPL.
Here's some code that watches Svelte dev events to maintain a component tree, and exposes it as a Svelte store for easy consumption by others. This code would need to run before your first Svelte component is created (or before the components you want to catch are created...).
import { current_component } from 'svelte/internal';
import { writable } from 'svelte/store';
const nodes = new Map();
const root = { children: [] };
// root components created with `new Component(...)` won't have
// a parent, so we'll put them in the root node's children
nodes.set(undefined, root);
const tree = writable(root);
// notify the store that its value has changed, even
// if it's only a mutation of the same object
const notify = () => {
tree.set(root);
};
document.addEventListener('SvelteRegisterComponent', e => {
// current_component is the component being initialized; at the time
// our event is called, it has already been reverted from the component
// that triggered the event to its parent component
const parentComponent = current_component;
// inspect the event's detail to see what more
// fun you could squizze out of it
const { component, tagName } = e.detail;
let node = nodes.get(component);
if (!node) {
node = { children: [] };
nodes.set(component, node);
}
Object.assign(node, e.detail);
// children creation is completed before their parent component creation
// is completed (necessarilly, since the parent needs to create all its
// children to complete itself); that means that the dev event we're using
// is fired first for children... and so we may have to add a node for the
// parent from the (first created) child
let parent = nodes.get(parentComponent);
if (!parent) {
parent = { children: [] };
nodes.set(parentComponent, parent);
}
parent.children.push(node);
// we're done mutating our tree, let the world know
notify();
// abusing a little bit more of Svelte private API, to know when
// our component will be destroyed / removed from the tree...
component.$$.on_destroy.push(() => {
const index = parent.children.indexOf(node);
if (index >= 0) {
parent.children.splice(index, 1);
notify();
}
});
});
// export the tree as a read only store
export default { subscribe: tree.subscribe }

Related

How to extend HTML attribute interfaces when designing reasonml react components?

I'm learning reasonml and quite excited about it. Something I often do in typescript react code is:
type Props = React.HTMLProps<HTMLButtonElement> & { foo: boolean }
const SuperButton: React.FC<Props> = (props) => <button {/* stuff with props */ />
In this regard, I communicate to my users as a component library provider that this button extends normal HTML button attributes.
How can I express and extend normal html component attributes in my components?
I see that reason explicitly doesn't support spreading props: https://github.com/reasonml/reason-react/blob/master/docs/props-spread.md.
I do see that there is a composition strategy: How to compose props across component in reason-react bindings?, but not sure how to marry that up with normal HTML element component stuffs.
Any recommendations? Thanks!
It's possible to do something similar using ReasonReact.cloneElement, as Amirali hinted. The idea is to split up your component's props and the HTML button's props into two separate parameters for your component, render your button, and then clone it while also injecting the extra button props.
This page shows a component which encapsulates this clone-and-injection functionality:
module Spread = {
[#react.component]
let make = (~props, ~children) =>
ReasonReact.cloneElement(children, ~props, [||]);
};
Now, you can use this Spread component for your SuperButton component:
module SuperButton = {
[#react.component]
let make = (~foo, ~htmlButtonProps) =>
<Spread props=htmlButtonProps>
<button> (foo ? "YES" : "NO")->React.string </button>
</Spread>;
};
The htmlButtonProps prop will contain the regular HTML button props, while separately foo is your component's specific prop. The component can be used like this:
<SuperButton foo=true htmlButtonProps={"autofocus": true} />
Small housekeeping note: you don't actually need to define the modules with the module keyword. If you want you can put them in separate files called Spread.re and SuperButton.re. Reason files automatically become modules.

Best way to send event from AngularJS parent component to Angular 7 child component? (ngUpgrade)

I have an AngularJS (1.7) app which is being migrated to Angular 7, using ngUpgrade. So the AngularJS and Angular frameworks are running at the same time, new components are written in Angular, and sometimes these modern Angular components are used inside of legacy AngularJS components.
In my case, the parent component needs to communicate with the child component in certain circumstances.
The ngUpgrade docs clearly show how to pass data and propagate events from the child Angular component to the parent AngularJS component:
<legacy-angularjs-component>
<div>{{ $legacyCtrl.whatever }}</div>
<modern-angular-child-component
[data]="$ctrl.initialDataForChildComponent"
(changed)="$ctrl.onChildComponentChanged($event)"
>
</modern-angular-child-component>
</legacy-angularjs-component>
To make that work, you just need to add a couple properties to the Angular child component: #Input() data; for the initial data, and #Output() changed = new EventEmitter<WhateverThing>(); that can then be used to propagate events to the parent component by doing this.changed.emit(this.whateverThing).
That all works, but what about propagating events from the parent to the child? I know how to do this in Angular, e.g. with #ViewChild or using observables, but those mechanisms are not available in my app's AngularJS environment. So the parent component cannot use them.
Two approaches I have tried that do work are:
Creating a separate service, which both components share.
Pass a reference to the parent controller into the child controller like this:
<legacy-angularjs-component>
<div>{{ $legacyCtrl.whatever }}</div>
<modern-angular-child-component
[observe]="$ctrl.registerObserver",
>
</modern-angular-child-component>
</legacy-angularjs-component>
...and then having the child component invoke this observe function:
ngOnInit() {
if (this.observe) {
// Pass reference to child component to the
// parent, so parent can directly send it messages.
this.observe(this);
}
}
This way, the parent has a direct reference to the child once the components are set up. The child implements some TypeScript interface that defines all the methods of the child that the parent can invoke to inform the child of events.
Both of those do work, but they both strike me as fairly kludgey and a lot of rigamarole to have to do for something as simple as sending an event to a child component.
Since this is easy to do in both AngularJS and Angular, I wondered if I might be missing an easier/simpler way to do the same thing in the context of ngUpgrade, where the parent is AngularJS and the child is Angular 7.
Or is my second approach a reasonable way to do it?
Well, as Eazy-E once said, "Ask, and ye shall immediately think of a simpler answer, right after you post the question in public."
A better solution than either of my first two mentioned above is to simply use regular Angular one-way bindings and use the OnChanges mechanism provided by Angular.
<legacy-angularjs-component>
<div>{{ $legacyCtrl.whatever }}</div>
<modern-angular-child-component
[observed]="$ctrl.valueThatChildIsObserving"
>
</modern-angular-child-component>
</legacy-angularjs-component>
Then, in the child component:
import { Component, Input, OnChanges, SimpleChanges } from '#angular/core';
export class MyChildCompoennt implements OnChanges {
#Input() observed: SomeWhateverObject;
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
// Inspect changes (it contains old and new values
// for `observed` object. That object could be a string
// or something more complicated with multiple properties.\
}
}
This way, when the parent component wants to tell the child to do something, it can just do this.valueThatChildIsObserving = someWhatever;.
Angular will handle invoking the child componentngOnChanges(), passing it a structure containing the old and new value of the property.
You have to implement the child component so that it can inspect the changed value and execute the correct action, but that is pretty simple and avoids having to have the parent/child share references to each other.
I think this is a reasonably simple way to implement "propagate some kind of event from parent component to child component" in a hybrid AngularJS/Angular app using ngUpgrade.

Issue with Dart WebUI autogenerated code

I have issue with auto generated code for web components.
Here is piece of HTML:
<div id="hidden-ui">
<div id="auth-form" class="...">
...
<to-button></to-button>
</div>
...
</div>
As you can see, there is custom web component called to-button:
<element name="to-button" constructor="TOSimpleButton" extends="div">
...
</element>
On startup I want to move #auth-form outside from parent node to document root:
Element af = document.query('#auth-form');
Element db = document.query('BODY');
db.children.add(af);
It's OK if there is no custom web-components inside movable node, but while to-button is inside I get run-time RangeError.
Here is piece of auto generated code:
__e1 = __root.nodes[9].nodes[1].nodes[7];
__t.component(new TOSimpleButton()..host = __e1);
As you can see, there is strict old path to component, thus RangeError exception raise.
How can I handle with this?
Sounds like you want to display popup forms every now and then. Here's what I do.
I specify this constructor for the dialog/popup:
var lifecycleCaller;
DialogFooComponent() {
host = new Element.html('<x-dialog-foo></x-dialog-foo>');
lifecycleCaller = new ComponentItem(this)
..create();
document.body.children.add(host);
lifecycleCaller.insert();
}
And as you can see, I add it to the document body. However, this only happens when creating a new instance.
Whenever I need to show that popup, I have code like this:
import '../dialog/foo/foo.dart';
...
// Later at some point I do:
new DialogFooComponent();
And what happens is that you have popup forms appearing in the body whenever you wish them to.
When you want to close the dialog, you can just call this inside the dialog component:
lifecycleCaller.remove();
As mentioned here, this will not be fixed in WebUI package, but will in Polymer.
this won't be fixed in web_ui pkg. It should work in polymer pkg.

Knockout 2: How to delay observable object.

Hi i have a problem in knockout 2: I want to do late binding because i am adding data-bind via jQuery
$("#button1").on ("click", function() {
lateBinding = $("#lateBindingElem);
if (lateBinding.length) {
lateBinding.attr("data-bind", "text: obs");
}
}
});
late binding is an html generated on the fly.
I have a view model created already call MyViewModel.
I want to add another attribute or another observable (could be computed or uncomputed) on the fly to existing view model? How would i do this?
Hopefully you have already found an answer elsewhere (7 months ago :D) but since I stumbled upon this question in hopes to find a solution to a similar problem, I might as well and try to give a sort-of-an-answer for anyone else looking into it. This won't let you manipulate the binding for elements you already have bound to a model but allow you to pause binding at given points and bind newly created elements to your current or a different viewmodel.
Built on Ryan Niemeyers great article about how to stop bindings and accompanying jsfiddle example is a little demo which adds new input elements to dom and binds them to different viewmodels.
Since you can only bind a section of your dom once you need to stop the downward binding at some point using a custom binding..
ko.bindingHandlers.stopBinding = {
init: function() {
return { controlsDescendantBindings: true };
}
};
assign it to a wrapper
<div data-bind="stopBinding: true" id="addNewContentHere"></div>
and insert your new elements
function addInput(){
var data=$('<input type="input" data-bind="value: newInput" />');
ko.applyBindings(MyViewModel, data[0]);
$('#addNewContentHere').append(data);
};
hope it is of some use :)

Knockout.js: Multiple ViewModel bindings on a page or a part of a page

I am wondering if it is possible to use Knockout.js's ko.applyBindings() multiple times to bind different ViewModels to one part of a page. For example, let's say I had this:
<div id="foo">...</div>
...
ko.applyBindings(new PageViewModel());
ko.applyBindings(new PartialViewModel(), $('#foo')[0]);
I am now applying two ViewModel bindings to <div id="foo>. Is this legal?
You do not want to call ko.applyBindings multiple times on the same elements. Best case, the elements will be doing more work than necessary when updating, worse case you will have multiple event handlers firing for the same element.
There are several options for handling this type of thing that are detailed here: Example of knockoutjs pattern for multi-view applications
If you really need an "island" in the middle of your content that you want to call apply bindings on later, then you can use the technique described here: http://www.knockmeout.net/2012/05/quick-tip-skip-binding.html
This is a common road block that comes when implementing JqueryMobile-SPA.
The method : ko.applyBindings(viewmode,root dom element) accepts two arguments. The second argument comes helpful when you have multiple VM's in your page.
for example :
ko.applyBindings(model1, document.getElementById("view1"));
ko.applyBindings(model2, document.getElementById("view2"));
where view1 and view2 are the root dom element for that model. For a JqueryMobile-SPA this will be the page ids for corresponding model.
The best way to do this would be use the "with" binding construct in the div that you want the partial view model to be bound. You can find it in this fiddle
<div data-bind="with: model">
<p data-bind="text: name"></p>
</div>
<div data-bind="with: anothermodel">
<p data-bind="text: name"></p>
</div>​
var model = {
name: ko.observable('somename'),
}
var anothermodel = {
name: ko.observable('someanothername'),
}
ko.applyBindings(model);​
Also check out the "with" binding documentation on the Knockout site, to look at an AJAX callback - partial binding scenario.
My english is very bad.... =)
I use Sammy to load partial views, and Knockout to bind the Model, I try use ko.cleanNode but clean all my bindings, all DOM nodes has changed when has a bind, a property __ko__ is aggregated, then i removed that property with this code, and works !!, '#main' is my node.
var dom = dom || $("#main")[0];
for (var i in dom) {
if (i.substr(0, 6) == "__ko__") {
delete (dom[i]);
break;
}
}
after use Ggle translator:
I use Sammy for the load of partial views, and Knockout for the bind the Model, I try to use ko.cleanNode but clean all my bindings, all DOM nodes has changed when they has a bind, a property ko is aggregated, then i removed that property with this code, and works !!, '#main' is my node.