Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Westbrook Johnson
Westbrook Johnson

Posted on

     

Your Portal Content through a LitElement

When last we met we were sending content throughportals as if we wereJack O'Neill sending soldiers to other galaxies. Not only that, we were doing it with vanilla javascript, thanks to the help ofShadow DOM andCustom Events, rather than with a framework as had some of the solid examples ofprior art that we checked out. If you haven't had the pleasure, or have forgotten much of what you read (join the club), don't worry, the rest of us will wait here for you...

...Your Content in Shadow DOM Portals...

...ok, now that we're all ready, there's no reason to bury the lede, today we're gonna talk about what those same techniques look like when taking advantage of the support of a simple base class for creating fast, lightweight web components;LitElement. And so, without further ado, here's what that looks like it all of its glory:

Well, maybe notall of its glory, more like in its one-to-one porting of the realities discussed and delivered with fully vanilla JS in the previous article. We've seen some of it before in theDeclarative API section of the previous article, but it's important to revisit it now as it will form the basis for extending the elements to support the ideas discussed across the wholeBut, now what? section therein. So, let's work up to full glory together!

Cross Browser Support

This was a large piece of any production possible code that I chose to leave out of our previous conversation for proof of concept's sake.We discussed some of the possibilities but didn't get into them, until now. The first place we'll run into an issue is with the use ofslot.assignedNodes(). You may remember we had previously been usingslot.assignedElements(), however, we want to be able to get loose text nodes as well as elements, soassignedNodes is the way to go. Let's take a look at what the code relying on this looks like now:

projectSlot(e){if(!e.target.assignedNodes().length)return;this.dispatchEvent(createEvent('portal-open',{destination:this.destination,content:e.target.assignedNodes(),}));}
Enter fullscreen modeExit fullscreen mode

You may also remember that when relying on ShadyDOM in a polyfilled setting there is no support forassignedNodes, so we'll need to do some extra work in order to enable the same functionality cross-browser. How sad that literally two lines of code charge such a tax on our goals here, but not to worry we can access similar results in this context with via[...el.childNodes]. While inmost cases this would do this trick, because of the use of a<slot /> tag with noname attribute we need to filter out a few possibly false positives before passing content on to our<portal-destination />.

getportalContent(){constslot=this.shadowRoot.querySelector('slot');returnslot&&slot.assignedNodes?slot.assignedNodes():this.childrenWithoutSlots;}getchildrenWithoutSlots(){letnodes=[...(this.childNodes.length?this.childNodes:[])];nodes=nodes.filter(node=>node.slot===''||node.slot===null);returnnodes;}projectSlot(){letcontent=this.portalContent;if(!content.length)return;this.dispatchEvent(createEvent('portal-open',{destination:this.destination,content:content,}));}
Enter fullscreen modeExit fullscreen mode

If you're interested in following along with the above code in real life, there are several ways you can access older browsers. The nuclear option is working with tools likeBrowserStack, or you could rely on one of the Virtual Machines that Microsoft offers forvarious versions of Internet Explorer and Edge, but my current go-to isFirefox: Extended Support Release. Firefox ESR is an enterprise-targeted release of the Firefox that is currently shipping version 60 which initially released before the v1 web components specification was supported by Firefox. It doesn't make debugging very fun, as I've not figured out how to open the dev tools, howeveralert() works just fine and I've leveraged it more than I'd like to admit...

In the realm of cross-browser support, the remaining context for us to cover is applying styles to the content when it reaches the destination end of the portal. This is really where things get tricky and force us to weigh the pros and cons of various paths forward. By defaultLitElement will do the work of ensuring the ShadyCSS is applied to components in a polyfilled context. ShadyCSS does the work to emulate shadow DOM based style encapsulation in browsers that do not yet support the specification natively, a list of browsers that grows shorter every day with the sun settings on IE11 and pre-Edgium Edge. It does so at the intersection of correctness and performance by writing a single scoped version of the styles targeted to the component in question into the global scope. This goes a long way towards maintaining the "styles scoped to element" contract of Shadow DOM based styles; however, it comes with two main trade-offs. The first involves not specifically addressing the "protected from external selectors" contract, which means that ALL styles from outside of your shadow DOM will have the ability to leak into your component. The second is more particularly troubling in the context of ourportal-destination definition, the styles applied to all instances of custom element's shadow DOM will have to be the same by default.

In that each piece of projected content over the lifecycle of an applicationcould be deserving of custom styling this can prove tricky in the context we've been working so far where we apply our content directly to the<portal-entrace/> element:

<portal-entrancedestination="style-demo"><style>button{background:red;}</style><h1>Send This Content</h1><p>Hello world! From my-element ${this.counter}</p><button@click=${this.increase}>+1</button></portal-entrance>
Enter fullscreen modeExit fullscreen mode

For the<style/>s defined in this context to apply to theportal-destination element, we need to do work over the top of theLitElement implementation to correctly scope this content via the ShadyCSS polyfill. What's more, the<style/> element would need to not be inside of theshadowRoot of a parent element at runtime to ensure it will not be consumed for by that parent element as if those styles were meant for it. The most direct way to overcome this issue is to wrap the content that we'd like to send over the portal in a custom element:

<portal-entrancedestination="destination"><content-to-be-ported-element></content-to-be-ported-element></portal-entrance>
Enter fullscreen modeExit fullscreen mode

However, the restrictions this places on potential use are quite prohibitive:

  1. Don't put<style/> elements directly into you<portal-entrance/>'s light DOM.
  2. When attempting to send style data across the portal, wrap it in an element, a la<content-to-be-ported-element/>.
  3. Manually wire state management between the parent element and the<content-to-be-ported-element/>.
  4. Etc.

While every well-defined piece of code requires a list of things you can't do with it, I feel this is a bridge too far. We should be able to dial these back a bit and allow us to ship this functionality with a little more flexibility. The main thing we are looking to address here is the ability to place<style/> elements directly into the<portal-entrance/> element and have those styles apply to the<portal-destination/> element to which they are sent. Luckily, whether you are using@webcomponents/webcomponentsjs/webcomponents-bundle.js or its slimmed down younger sibling@webcomponents/webcomponentsjs/webcomponents-loader.js to ensure cross-browser support they will each ensure that browsers without native shadow DOM support are delivered the ShadyCSS polyfill.

The ShadyCSS polyfill supplies an API by which templates and styles can be prepared to approximate the encapsulation of the content in our similarly polyfilled shadow root from the rest of the document. We can use it to do additional work of it over the top of what is provided byLitElement in order to ensure the same treatment of<style/> content sent over our portal. The process involves these steps:

  1. Ensure that either the polyfill is not present or that it is not presently being used.
  2. Create a template to prepare the styles in.
  3. Gather all of the<style/> tags that will be direct children on the<portal-destination/> element. Capture both their style text (innerHTML) for scoping and append the nodes to the template created above for preparing the DOM.
  4. Early return if no<style/> tags have been found.
  5. Use ShadyCSS to scope the gathered CSS text and prepare the template to apply those scoped styles.
  6. Forward the non-HTMLStyleElement elements to be appended to the<portal-destination/> element.

This looks like the following in code:

getpreparedProjected(){if(!this.projected)return[];if(window.ShadyCSS===undefined||window.ShadyCSS.nativeShadow){returnthis.projected;}letstyles=[];lettemplate=document.createElement('template');this.projected.filter(el=>el.constructor===HTMLStyleElement).map((s)=>{styles.push(s.innerHTML);template.appendChild(s);});if(styles.length===0){returnthis.projected;}template.innerHTML=stylesHTML.join('');window.ShadyCSS.ScopingShim.prepareAdoptedCssText(styles,this.localName);window.ShadyCSS.prepareTemplate(template,this.localName);window.ShadyCSS.styleElement(this);returnthis.projected.filter(el=>el.constructor!==HTMLStyleElement);}
Enter fullscreen modeExit fullscreen mode

This means that our usage caveats are much more acceptable:

  1. You cannot have<style/> element openly available for consumption by a parent component at runtime.
  2. Only<style/> elements that are direct children will apply to the light DOM content of an "entrance".
  3. <style/> elements directly in the<portal-entrance/> light DOM will apply to all<portal-destintion/> elements and their content, regardless ofname.

Note: I would expect styles applied this way torougly follow the cascade (it seems they get added in reverse cascade 😖, but the current state of the ShadyCSS library does not agree. I've created the followingissue to hopefully bring some agreement between the expectation and the reality here.

With these alterations, our family of portal elements is now ready for delivery cross-browser no matter the level of support those browsers have for the Shadow DOM specification. This capability came with some active trade-offs, but as they are directly in line with those that come with the ShadyCSS polyfill itself, which means they will hopefully be familiar to those working with other web components and shadow DOM tools.

When you bring this all together in an updated version of ourMenu Populates Content Populates Menu Example from the previous article, it looks like the following in all its cross-browser supporting glory:

A close eye might catch the use ofno-shadow on the<html-include-with-anchors/> elements, there is some sort of timing issue in upgrading there<portal-entrance/> elements therein that I'm gonna have to follow up on separately.

From this baseline, we can now focus on rounding out some of the capabilities of our portal.

Declarative API

The ability to dynamically track the attributes of an element without any special APIs for setup is certainly one of the clearest wins of the custom element specification. Through the use of the staticobservedAttributes array and the associatedattributeChangedCallback we are able to take fine-grained control over how our components react to changes declared directly in the markup describing them. That means the following code allows our newly defined custom element to react to changes in the value of thecustom-attribute attribute and store that value as a local property.

classDeclarativeElementextendsHTMLElement{staticobservedAttributes=['custom-attribute'];attributeChangedCallback(name,oldValue,newValue){switch(name){case'custom-attribute':this.customProperty=newValue;break;}}}
Enter fullscreen modeExit fullscreen mode

Others have previously pointed out that managingALL of your attributes and their relationship to properties in this manner can be quite tiresome, and I would agree. Not having to manually wire everything you want to track in the HTML of your custom element to related properties one at a time is a great reason to work with libraries and tooling when developing web components. Luckily, we're already committed to usingLitElement as a base class which helps us setup this relationship via itsstatic get properties() API. Let's take a look at how the above is achieved therein:

classDeclarativeElementextendsLitElement{staticproperties={customProperty:{type:String,attribute:'custom-attribute'}}}
Enter fullscreen modeExit fullscreen mode

Notice the change fromHTMLElement toLitElement for our class extension. That change gives us access to a static properties getter that will outline the attributes we want to hear about changes to, and we receive anextended list of options with which you can outline the relationship between the attributes and their associated properties. For our<portal-entrace/> element, we can outline a more declarative API, like so:

classPortalEntranceextendsLitElement{staticproperties={destination:{type:String},manual:{type:Boolean},open:{type:Boolean,reflect:true},order:{type:Number},}}
Enter fullscreen modeExit fullscreen mode

Adding a property in this way to aLitElement based custom element also means that changes to these properties will automatically kick off the update lifecycle of the component. In the case that these properties are used in building the DOM representation of your element, this is super helpful. However, being that none of these properties need to trigger a new render there are a couple of paths to optimizing reactive management of these attributes. We could extend these definitions to includehasChanged() { return false; } and prevent that entirely. Or, we could separately use theshouldUpdate lifecycle method to prevent that holistically across the component. Further, knowing that there is zero processing that goes into understanding our element's template of<slot @slotchange=${this.shouldProjectSlot}></slot>, we can rely onlit-html, the renderer underlyingLitElement, to efficiently discover that there are no DOM changes to be made after any of those changes and not worry about extended configuration at all. So many options towards ensuring a more performant application! To ensure that our<portal-entrance/> elements are rendered once and then not worried about again, we'll pair theshouldUpdate and thefirstUpdated lifecycle methods like so:

shouldRender(){return!this._hasRendered;}firstUpdated(){this._hasRendered=true;}
Enter fullscreen modeExit fullscreen mode

Here, our first update occurs unimpeded but by settingthis.shouldRender() = false as part of that first update, no further updates to the rendered shadow DOM are made.

Right about now you might be asking, "If they don't trigger a render, what do these propertieseven do?", and with good reason! First, let's remember that all of the DOM related to our portal is supplied as light DOM, and we use the<slot/> element in our template to listen to changes in that content for sending across the portal, which means internally we only need to render once, as shown above. When changes in the light DOM content occur, a call toshouldProjectSlot() will be made, which is where our component decides what to do with the DOM provided:

shouldProjectSlot(){if(!this.open){if(!this.manual){this.open=true;}}elseif(this.manual){this.projectSlot();}}
Enter fullscreen modeExit fullscreen mode

The most important thing to take away from this transaction is that whenmanual === true andopen === true theprojectSlot() method will be called directly allowing content placed into<portal-entrance/> to be streamed across the portal. Otherwise, whenmanual === false,open is set totrue, which relies on the following getter/setter pair:

getopen(){returnthis._open;}setopen(open){if(this.open===open)return;this._open=open;if(open){this.setAttribute('open','');this.projectSlot();}else{this.removeAttribute('open');this.close();}}
Enter fullscreen modeExit fullscreen mode

Within this setter we eventually make that call toprojectSlot() in this context as well, we just make a short detour to maintain a representative state on the way there. This allows us to worry about the fewest number of entries into the projection functionality as possible while also aligning the internal API of the<portal-entrace/> element with that available from the outside.

We'll match this with declarative updates to the API of our<portal-destintion/> element as well. These additions will leave our static properties getter looking like the following:

classPortalDestinationextendsLitElement{staticproperties={name:{type:String},projected:{type:Array},multiple:{type:Boolean},announces:{type:Boolean},projecting:{type:Boolean}}}
Enter fullscreen modeExit fullscreen mode

Much of these additions will be discussed in greater depth along with the features they add below, but, before we move on, notice theprojecting property. We'll use this in conjunction with theprojecting attribute as a hook for styling this component when content is being projected into it. This being purely representational of internal state, it will be helpful to prevent this from being changed from the outside. While techniques like the use of underscore prefixed ornew Symbol() based property names can support this sort of security, we can also manage this reality by only offering a setter for this value:

setprojecting(projecting){projecting=this.projected.length>0;if(projecting){this.setAttribute('projecting','');}else{this.removeAttribute('projecting');}}
Enter fullscreen modeExit fullscreen mode

Here we receive an incoming value and simply throw it away. At this time, I don't see needing this property for anything other than the styling hook, so we don't even need to cache it internally. In theupdated() lifecycle method we'll usethis.projecting = 'update'; to initiate this functionality, and the setter will manage the presence of theprojecting attribute.

With our declarative API prepared, controlling theopen state anddestination of a<portal-entrance/> becomes very straight forward. See it in action below:

Multiple Entrances

Now that we are more practiced on delivering the API for our portal in a declarative manner, doing so for additional features will hopefully become less and less daunting. One piece of functionality that we've previously discussed supporting and that can benefit from a declarative API is the ability to project content from more than one<portal-entrance /> into a single<portal-destination/>; another feature originally outlined by thePortal Vue project. We can power this with the addition of amultiple attribute to our<portal-destination/> element, as well as anorder attribute to our<portal-entrance/> element. Usage might appear like this following:

<portal-entrancedestination="mutliple"order="1"><h1>Second</h1></portal-entrance><portal-entrancedestination="mutliple"order="0"><h1>First</h1></portal-entrance><portal-destinationmultiplename="mutliple"></portal-destination>
Enter fullscreen modeExit fullscreen mode

In the above example, both of the<h1/> elements will be sent to the<portal-destination/> and due to the presence ofmultiple, both will be displayed therein. However, because of the values in theorder attributes for those<portal-entrance/> elements, the first<h1/> will be displayed second, and the second<h1/> will be displayed first. To make this possible, we've added theorder attribute to the static properties getter in our "entrance" element:

order:{type:Number}
Enter fullscreen modeExit fullscreen mode

With that attribute surfaced at the API level, it will then be available for delivering to our "destination" element via theportal-open:

projectSlot(){letcontent=this.portalContent;if(!content.length)return;this.dispatchEvent(createEvent('portal-open',{destination:this.destination,content:content,entrance:this,order:this.order||0,}));}
Enter fullscreen modeExit fullscreen mode

On the "destination" side, there will be a good bit more that needs to change to support this addition. Before we get into those, we'll need to add the new attribute to its properties getter:

multiple:{type:Boolean}
Enter fullscreen modeExit fullscreen mode

Once again, this allows us to receive changes to this attribute via theattributeChangedCallback thatLitElement connects directly to a matching property. With that available in our custom element, we'll then be able to use it to make decisions on how to respond to the various events that are being listened for. Specifically, we'll change theupdatePortalContent method from being a catch-all for the most recently opened/closed<portal-entrance/> element to a gate for managing content differently depending on the value ofmultiple:

updatePortalContent(e){this.multiple?this.portalContentFromMultiple(e):this.portalContentFromOne(e);}
Enter fullscreen modeExit fullscreen mode

That simple, right? Riiight.

To support both of these code paths, we'll create an intermediary map to cache the available content before flattening it into an array of arrays for pushing into our template. This means we'll create anew Map() that will be keyed by the actual<portal-entrance/> elements from which the content is delivered. The values will be structured as an object with both the received content, as well as the order value from the "entrance" element:

{portal-element=>{content:node[],order:number,}}
Enter fullscreen modeExit fullscreen mode

We'll build this data in response to theportal-open event via the following method:

cacheByOriginOnOpen(e){if(e.type!=='portal-open')return;this.projectedByOrigin.set(e.detail.entrance,{content:e.detail.content,order:e.detail.order,});}
Enter fullscreen modeExit fullscreen mode

We'll use this map in themultiple === false path of ourupdatePortalContent functionality to decide whether the "destination" is currently receiving content from an "entrance" and to close that entrance before applying new content to the destination:

portalContentFromOne(e){if(this.projectedByOrigin.size){this.projectedByOrigin.keys().next().value.open=false;}this.cacheByOriginOnOpen(e);this.projected=e.detail.content||[];}
Enter fullscreen modeExit fullscreen mode

And, on themultiple === true path, the map will power our ability to sort the content by theorder attribute delivered from the "entrance" and flatten the map into our expectedprojected property:

portalContentFromMultiple(e){this.cacheByOriginOnOpen(e);constbatchProjected=Array.from(this.projectedByOrigin.values());batchProjected=batchProjected.sort((a,b)=>a.order-b.order).reduce((acc,projection)=>{acc.push(projection.content);returnacc;},[]);this.projected=batchProjected;}
Enter fullscreen modeExit fullscreen mode

Whenportal-close is dispatched, we'll use this structure to ensure only the content in question is being returned to the closing<portal-entrance/> element while also removing that element from the local cache before updating the portal content once again:

closePortal=(e)=>{if(!this.confirmDestination(e))return;this.returnProjectedWhenManual(e);this.projectedByOrigin.delete(e.detail.entrance);this.updatePortalContent(e);}returnProjectedWhenManual({detail:{manual,entrance}}){if(!manual)return;constprojected=this.projectedByOrigin.get(entrance);if(!projected)return;projected.content.map(el=>entrance.appendChild(el));}
Enter fullscreen modeExit fullscreen mode

In an actual application, this could exhibit a list of items for multiple selected with the<portal-destination/> playing the role of confirmation UI, allowing it to be located anywhere on the page. In the following example, the "selected" list will appear directly next to the ten options. However, in the DOM, the two lists are in completely different branches:

Mirrored Listening

Up to this point we've relied on our<portal-destination/> elements being live and named when our<portal-entrance/> elements come knocking with theirportal-open events. Paired with our recent addition of themanual attribute outlined above, this seems like a fairly complete API relationship between the two elements. However, what if our "entrance" is ready toopen before out "destination" is ready toreceive? Whether through general runtime realities or as applied consciously when taking full control of your application's load process, it is feasible that you will run into a context where you intend for a<portal-destination/> to be lying in wait when youopen a<portal-entrace/> and it's just not there. To support this, let's add some functionality to "announce" the presence or a change of name in our "destination" element. It's a great addition to the declarative API of our elements, we can do so, while also making it opt-in, by adding anannounces attribute to our<portal-destination/> element. While we're at it, let's also make thename attribute reflect so that any changes we make to that value imperatively will be represented in the rendered DOM.

name:{type:String,reflect:true,},announces:{type:Boolean,}
Enter fullscreen modeExit fullscreen mode

WithLitElement we have a couple of options as to where we'd like to react to changes in our properties. In this case, we can get all of the flexibility that we'll need by relying on theupdated lifecycle method. There we will receive a map keyed by values that have changed pointing to the previous value of those properties. This will allow us to test for changes to eitherannounces orname withchanges.has(), like so:

updated(changes){if(changes.has('announces')){this.shouldAnnounce();}elseif(changes.has('name')&&typeofchanges.get('name')!=='undefined'){this.announce();}this.projecting='update';}
Enter fullscreen modeExit fullscreen mode

In the case of changes toname, when the value is being changed (not when being set initially fromundefined) we'll immediately make a call toannounce() the presence of the<portal-destination/> element. When it is the value ofannounces that has changed we'll make a call toshouldAnnounce() which confirmsannounces === true before callingannounce(). This path is also added to theconnectedCallback so that when the element is rejoining the DOM it will also announce itself when configured to do so.

announce(){this.dispatchEvent(createEvent('portal-destination',{name:this.name,}));}
Enter fullscreen modeExit fullscreen mode

As you can see, theannounce method is powered again by Custom Events, this time theportal-destination event. On the<portal-entrance/> side we'll listen for that event, using a listener attached to thedocument and thecapture phase of that event so that it can respond accordingly with as little interference as possible:

connectedCallback(){super.connectedCallback();document.addEventListener('portal-destination',this.destinationAvailable,true);}disconnectedCallback(){super.disconnectedCallback();document.removeEventListener('portal-destination',this.destinationAvailable,true);this.open=false;}destinationAvailable=(e)=>{if(e.detail.name===this.destination){this.shouldProjectSlot();}}
Enter fullscreen modeExit fullscreen mode

And now we're listening on both sides of the portal. Our already thorough API is even more complete and we've further expanded the ways we can leverage our component manages content and the way it can display throughout our application. While it's not always easy to anticipate how realities of the loading process will affect the performance of our applications, in the following demo I've artificially delayed thecustomElements.define() call for the<portal-destination/> element so that you can experience what this enables. Run the demo with the console open to follow along on the delayed timing:

Even More Styles

With the support for style application that we added as part of our cross-browser coverage, we now have a lot of control over how we style the content that we're sending over the portal. Styles contained within child components of our<portal-entrance/>s forwarded to our<portal-destination/>.<style/> tag children of those "entrances" are also forwarded to their assigned "destination", assuming that when ShadyCSS is required those elements are added after the<portal-entrance/>'s parent element's shadow DOM was initially polyfilled. However, when working with custom elements and shadow DOM we are offered an even wider array of possibilities to style our DOM.

There are some newer ways like working withConstructible Stylesheets, and the number of immediate performance benefits they bring. In concert with theadoptedStyleSheet API, they also open anexpanded set of possibilities when working within predefined style systems. There are also more common concepts that need to be addressed like CSS Custom Properties.

The way that they offer a style bridge into the shadow DOM of a custom element is really powerful. However, when physically moving DOM from one part of the DOM tree to another it can take that content out of the cascade which those custom properties rely on to be applied appropriately. With those custom properties being difficult to acquire without previous knowledge of their presence, it is tricky to find productive/performant ways to move those properties along with the content that is being sent across the portal. These concepts and more being ripe for research, a follow-up article specifically covering style acquisition and application seems appropriate, even before this one is even done.

But, now what?

Beyond simply porting our<portal-entrance/> and<portal-destination/> elements to extending theLitElement base class, we've already done so much:

  • prepared the elements for delivery cross-browser
  • surfaced a declarative API
  • added support to display content frommultiple "entrances" in a single "destination"
  • created a bi-directional relationship between the two elements so that the portal can open regardless of which is ready first

But, there is still so much to do!

Even before getting into the experimental work around supporting a more rich style application ecosystem, the most important next step is the addition of testing. Even just developing the demos for this article I found a number of edge cases that will need to be fully covered to call these components "production ready". I've done my best to fill in the holes as I wrote, but I'm sure there are things that I've missed and updates not appropriately reflected in this article. Focusing on the integration point between these two elements, there is much to be done in order to ensure future additions and refactoring do not affect the functionality we've worked on so far negatively. To that end, I will be spending some quality time withTesting Workflow for Web Components before getting back to you all with even more explorations on the other side of the portal. Try not to close the "entrance" while I'm gone.

Top comments(3)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
hyperpress profile image
John Teague
#HyperPress project, @google lit-element, #WebComponents, Headless WordPress. Web performance junky. Founder and CTO of Logical Phase.
  • Location
    Minneapolis, MN USA
  • Education
    MA History
  • Work
    Founder and CTO at Logical Phase Systems
  • Joined

Brilliant WJ! Great to see expanded demonstrations using LitElement.

CollapseExpand
 
aigan profile image
Jonas Liljegren
Building modern web components on reactive state semantic graphs.Passionate about exploring unconventional methods in technology development to shape a better future.
  • Location
    Göteborg
  • Joined

Is this published anywhere?

CollapseExpand
 
westbrook profile image
Westbrook Johnson
  • Location
    Brooklyn
  • Work
    Founding Frontend Engineer at something new
  • Joined

The code itself, no. Feel free to take it straight from the example, if you'd like...

The concept is built intoopensource.adobe.com/spectrum-web-... which might be of interest.

At current, I'm investigating a future version of this leveraging<dialog> and thepopup attribute and hopefully CSS Anchoring...once it's fully up to snuff, hopefully I'll have time to write about it as well!

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Location
    Brooklyn
  • Work
    Founding Frontend Engineer at something new
  • Joined

More fromWestbrook Johnson

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp