thingsinjars

  • 6 Jun 2020

    Line-by-line: Flat Cube Web Component

    Line-by-Line breakdowns go into excessive – and sometimes unnecessary – detail about a specific, small project. Be prepared for minutiae.

    Here, I'll go through the FlatCube web component line-by-line. It is a single web component that draws a flattened-out representation of a Rubik's Cube (or other 3x3 twisty puzzle) based on a string passed in that describes the positions of the pieces. A cube contains six faces. A face contains nine pieces.

    You'll probably want to have the full code open in another window to see this in context.

    The component

    Usage

    <flat-cube facelet="UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB" />

    Line-by-line

    First, all WebComponents must extend the HTMLElement class. If you're building with a library such as LitElement, you might extend a different class but that class will ultimately extend HTMLElement.

    class FlatCube extends HTMLElement {

    The constructor is called every time a FlatCube element is created, not just once per page load.

      constructor() {

    We have to call the constructor on HTMLElement so that all the behind-the-scenes plumbing is taken care of.

    NOTE: If we don't do this, we can't use this.

        super();

    Now set up some internal variables for the FlatCube itself. this.faces becomes an array representing the order of faces in the facelet string.

        this.faces = 'URFDLB'.split('');
    
        this.facelet = false;

    We attach the shadow DOM to this element so that we can access it easily during the render phase.

        this._shadowRoot = this.attachShadow({ mode: 'open' });

    Then we create the template (see below) and trigger the first render to actually show the element on screen.

        this.template = this.createTemplate();
        this.render();
     }

    Style

    It isn't essential to separate the component's CSS into another method but I like to do it to keep everything nice and tidy.

    style() {

    By using template literals, we can write a block of plain CSS.

        return `

    The :host CSS selector references the element itself. It's kinda like this but in CSS.

    NOTE: It can only be used from inside the Shadow DOM

    :host {

    I want this component to be able to be used inline or as a block item so I'm specifying inline-block. If the context it ends up being used in requires it to be block, it's possible to wrap it in another element.

      display: inline-block;

    Skinning and scaling

    In this web component, one of the main goals of the implementation was the ability to easily change the size of the component and the colours of the faces.

    Luckily, CSS variables make it super easy to make components skinnable and the calc function is very useful for scaling.

    The base measurement

    All the dimensions – the full component width, the faces, the individual pieces – are multiples of the --flat-cube-face-width value. This is passed in from the containing CSS by specifying a value for --flat-cube-face but if it is not specified, we want a fallback of 100px.

      --flat-cube-face-width: var(--flat-cube-face, 100px);
    }

    Now the styles for the complete element. Set position to be relative so that we can absolutely position the individual faces.

    .flat-cube {
      position: relative;

    And specify the element to be the height of 3 faces and the width of 4. This is where the calc function comes in handy, especially in a web component intended to be reusable and seamlessly scalable.

      height: calc(3 * var(--flat-cube-face-width));
      width: calc(4 * var(--flat-cube-face-width));

    I'm using outline rather than border for the lines between the pieces so I want to add a 1px margin around the outside to prevent clipping.

      margin: 1px;
    }

    Each individual face shares the same class

    .face {

    Use the value passed in as the base measurement.

      height: var(--flat-cube-face-width);
      width: var(--flat-cube-face-width);

    Each face is absolutely positioned inside the containing .flat-cube element.

      position: absolute;

    But rather than specify exact positions for each individual piece in a face, we use flexbox to lay them out automatically. We draw the pieces in order then let them wrap onto the next line,

      display: flex;
      flex-wrap: wrap;

    I wanted to specify the width of each piece as a simple ⅓ of the face width. In order to do that, I used outline rather than border as border actually takes space in the element where outline doesn't.

      outline: 1px solid var(--flat-cube-outer, black);
    }
    

    These are simply the top and left positions of the individual faces. We don't really need to go line-by-line here.

    .U-face {
      top: 0;
      left: var(--flat-cube-face-width);
    }
    .L-face {
      top: var(--flat-cube-face-width);
      left: 0;
    }
    .F-face {
      top: var(--flat-cube-face-width);
      left: var(--flat-cube-face-width);
    }
    .R-face {
      top: var(--flat-cube-face-width);
      left: calc(2 * var(--flat-cube-face-width));
    }
    .B-face {
      top: var(--flat-cube-face-width);
      left: calc(3 * var(--flat-cube-face-width));
    }
    .D-face {
      top: calc(2 * var(--flat-cube-face-width));
      left: var(--flat-cube-face-width);
    }
    

    Using the child selector to access the pieces inside the face.

    .face > div {

    As I mentioned above, we want to calculate the width of the pieces simply as ⅓ of a face so we use outline. The alternative would be to calculate the pieces as (⅓ of (the face width minus 2 * the internal border width)). That sounds mistake-prone.

      width: calc(var(--flat-cube-face-width)/3);
      height: calc(var(--flat-cube-face-width)/3);
      outline: 1px solid var(--flat-cube-inner, black);
    }

    Again, this is just colours. We don't need to go line-by-line. The only thing to note is that each piece has a fallback colour specified in case the containing application doesn't pass one in.

    .U-piece {
      background-color: var(--flat-cube-up, #ebed2b);
    }
    .L-piece {
      background-color: var(--flat-cube-left, #ff6b16);
    }
    .F-piece {
      background-color: var(--flat-cube-front, #6cfe3b);
    }
    .R-piece {
      background-color: var(--flat-cube-right, #ec1d35);
    }
    .B-piece {
      background-color: var(--flat-cube-back, #4db4d7);
    }
    .D-piece {
      background-color: var(--flat-cube-down, #fffbf8);
    }

    And finally, we close off our template literal and end the style method.

    `;
      }

    Template

    Now we build up the actual DOM of the element. We've done a lot of styling so far but, technically, we've nothing to apply the styles to. For that, we're going to build up the structure of faces and pieces then attach the styles.

      structure() {

    At this point, we have a couple of choices. We can either build this structure once and update it or build it fresh every time we need to make a change. The latter is easier to write but the former has better performance. So let's do that.

    This is the createTemplate method we called in the constructor. It is called only once for each instance of the component so we don't need to go through the whole building process every time.

    createTemplate() {

    First, create a new template. Templates are designed for exactly this case – building a structure once and reuse it several times.

        const template = document.createElement('template');

    Then we attach the styles we defined earlier:

        template.innerHTML = `<style>${this.style()}</style>`;

    And, finally, actually create the first element that actually appears in the component. This is the div that contains everything. We also add the .flat-cube class to it.

        const cubeElement = document.createElement('div');
        cubeElement.classList.add('flat-cube');

    The this.faces array we defined in the constructor comes back here. We loop over each face we require and create the DOM for it.

        this.faces.forEach((face, i) => {

    A div to contain the face with the shared .face class for the size and the specific class for the position and colour – .U-face, .B-face, etc.

          const faceElement = document.createElement('div');
          faceElement.classList.add('face');
          faceElement.classList.add(`${face}-face`);

    Now we create the individual pieces. If we wanted to make this component customisable so that it could represent cubes with a different number of pieces(2x2, 4x4, 17x17, etc.), we'd use a variable here instead of 9.

          for (let j=0; j < 9; j++) {

    Now we call out to the preparePiece method (see below) without an element to make sure the piece has the right class assigned to it before we append the piece to the face.

            faceElement.appendChild(this.preparePiece(i, j));
          }

    By the time we get here, we have a face div with 9 piece divs appended. Now we can add that to the cube.

          cubeElement.appendChild(faceElement);
        });

    Do that for each face and we have a div containing a completed flat-cube which we can append to the template.

        template.content.appendChild(cubeElement);

    And return the template to the constructor.

        return template;
      }

    Updating

    Now we have the structure in a template, we can grab a copy of it any time we need to update the cube.

      updateTemplate() {

    Passing true to cloneNode means we get a deep clone (containing all the nested faces and pieces) rather than a shallow clone (just the top-level element).

        const update = this.template.content.cloneNode(true);

    We loop over each face and then each piece (div) in each face to update it.

        update.querySelectorAll('.face').forEach((face, i) => {
          face.querySelectorAll('div').forEach((piece, j) => {

    We're using the same method here as we did to create the individual pieces (code reuse is A Good Thing) but this time we're passing in the piece we already have rather than asking the method to create a new one.

            this.preparePiece(i, j, piece);

    The update variable now contains an updated DOM representing the current facelet string.

          });
        });
        return update;
      }

    This method takes the i (index of the face) and j (index of the piece) we need to figure out which colour this piece needs to be. It also takes an optional argument of piece. If we don't provide that – the way we do in the initial createTemplate call – piece will be a newly created div. If we do provide that argument, we'll update whatever is passed in instead.

      preparePiece(i, j, piece = document.createElement('div')) {

    We have to map the facelet string – default: "UUUUUUUUU...etc" – into the two-dimensional structure of faces and pieces. Okay, technically, it's a one-dimensional mapping of a two-dimensional mapping of a three-dimensional structure. But... let's just not.

        const start = (i * 9) + j;
        const end = (i * 9) + j + 1;
    

    This means "If we don't have a facelet string, just colour the piece according to what face it is in, otherwise, colour it according to the i,j position in the facelet string".

        piece.className = !this.facelet ? `${this.faces[i]}-piece` : `${this.facelet.slice(start, end)}-piece`;

    Once we've updated the piece, return it so it can be included in the content.

        return piece;
      }

    We call this method in the constructor and every time we want to update.

      render() {

    Empty out the element content

        this._shadowRoot.innerHTML = '';

    And replace it immediately with the content we generate with the update method from above.

        this._shadowRoot.appendChild(this.updateTemplate());
      }

    This is how we register the component to listen for changes. This getter returns an array listing the attributes we want to listen for.

      static get observedAttributes() {

    There's only one attribute we care about listening to. Any change to the facelet attribute will cause us to re-render.

        return ['facelet'];
      }

    The other part of registering for changes to the attributes. This is the handler that is invoked with the name of the changed attribute (useful when you're listening to a lot of attributes), the oldValue (before the change) and the newValue (after the change).

      attributeChangedCallback(name, oldValue, newValue) {

    We only care about the newValue because we've only registered a single listener and we don't need the oldValue.

        this.facelet = newValue;

    Then we trigger a new render to update the state of the element.

        this.render();
      }

    And we close out our FlatCube class.

    }

    Listening for events

    The final part of the puzzle is to register our new element with the browser so that it knows what to do when it sees our element. We do this by passing our new element to the CustomElementRegistry.

    I like to check if my element has already been registered. Without this, including the script twice by accident will trigger an error that the user of the component isn't necessarily going to recognise.

    if (!window.customElements.get('flat-cube')) {

    To register, you pass the tag name you want to use and the element class.

    NOTE: tag names for custom components must contain a hyphen.

      customElements.define('flat-cube', FlatCube);
    }

    And that's it. Every line looked at and, hopefully, explained.

    Let me know what you would have done differently or if there are any (ideally small) projects you'd like me to look at line-by-line.

    Geek, Development, Javascript, CSS

  • 3 Jun 2020

    Application Layers

    On recent web app projects in HERE Tracking, I've been using a layered component structure that fits particularly well with frontends that access and interact with JSON APIs.

    The primary reason for structuring our apps this way is that it gives us a lot of freedom in our workflow and still fits well within the larger HERE structure with horizontal design teams that align across the multiple products. This works as a way to enable parallel contributions from everybody across the engineering teams.

    The layers are:

    • Application
    • Library (JS library to access the API)
    • Logical (maps business objects to layout concepts)
    • Layout (renders layout components)
    • Components (low level elements and design)

    And, generally, these layers are within the areas of expertise of the Backend, Frontend and Design specialists.

    It shouldn't be necessary to say this but just to make sure I'm not misunderstood: it's important to note that none of these roles is limited to the scope below, this is just a general 'areas of expertise' guide. See my previous post about shared responsibilities.

    • Backend teams create the API and implement the JS library. If possible, also implement the basic logical component which performs whatever business logic is required.
    • Frontend teams build the application out of components, further maintain the logical components and the mapping between logical and layout components
    • Design teams implement the core web components and company-wide design system of UX, UI, mental models, etc. This layer can also be based upon an open-source design system such as Carbon or Material.

    Of course, the backend team can modify the web components if they have the inclination just as the design team couuld make improvements to the database if they are able to improve the product.

    Example

    NOTE: The example below is mostly Vue-like but this layered approach doesn't rely on any framework, language or coding style. It's a way to split and share responsibilities.

    Rubber Duck Inc. make GPS-enabled rubber ducks. They have a dashboard where customers can see the location of their ducks. The dashboard includes an overview list of ducks.

    Backend

    The Backend team extend the Duck definition (stored in their duck-ument database) to include a new 'icon' field then update the GET /ducks endpoint that allows you to receive a list of all the ducks you own.

    Sample response:

    {
      "data": [{
        "id": 123,
        "name": "Hugh",
        "colour": "hotpink,
        "icon": "star",
      }, [{
        "id": 321,
        "name": "Anthony",
        "colour": "yellow",
        "icon": "dot",
      }],
      "count": 2
    }
    

    They check to see if the JS library needs updating (if they are using automated code generation, this might already be done). It doesn't, it already returns the full data array of the response:

    fetch(`${api}/ducks`)
      .then(response => response.json)
      .then(json => json.data)
    

    The data is rendered in the web app using a logical web component

    <duck-list :ducks="ducks"/>

    The engineer digs one step deeper (into the 'logical' or 'application components' library) and sees that the duck-list component wraps the generic-list component but with a few modifications to the data structure.

    <template>
      <generic-list :items="items"/>
    </template>
    <script>
      :
      props: {
        ducks: Array,
      },
      data() {
        return {
          items: this.ducks.map(duck => ({
            title: duck.name,
            subtitle: `This duck is ${duck.colour}`,
          }))
        };
      },
      :
    </script>

    And then modifies it to also pass the icon into the generic-list so that each item looks like:

    {
      title: duck.name,
      subtitle: `This duck is ${duck.colour}`,
      icon: duck.icon
    }
    

    Frontend

    In a parallel task, the frontend specialist can be improving the generic-list component. This component doesn't do much except create a set of generic-list-item elements.

    <template>
      <ul>
        <generic-list-item for="item in items" :item="item">
      </ul>
    </template>

    Each generic-list-item is built from basic web components from the company's DuckDesign language:

    <template>
      <li>
        <rubber-duck-title>{{title}}</rubber-duck-title>
        <rubber-duck-subtitle>{{subtitle}}</rubber-duck-subtitle>
      </li>
    </template>

    Frontend can then improve this to take advantage of the new data structure. Handily, there's a rubber-duck-avatar component. That should work here:

    <template>
      <li>
        <rubber-duck-avatar if="icon">{{icon}}</rubber-duck-icon>
        <rubber-duck-title>{{title}}</rubber-duck-title>
        <rubber-duck-subtitle>{{subtitle}}</rubber-duck-subtitle>
      </li>
    </template>

    Design

    So close, except the alignment's not quite right... Frontend has a chat with design and they decide that, while this could be solved in the generic-list-item component (or even in the duck-list or the application layer), having an icon next to a title is a more generic requirement so it should be solved in the lowest design component layer:

    rubber-duck-avatar + rubber-duck-title {
      margin-left: 0;
    }
    

    Design tweaks the alignment of the rubber-duck-avatar component and deploys it company-wide to all product teams. Every team benefits from the shared library, the DuckDashboard team gets to show off their new duck icons, everybody helped complete the product story and nobody got hurt.

    Conclusion

    Admittedly, this does lead to having multiple individual repositories for a single application

    • dashboard-app
    • duck-api.js
    • dashboard-components
    • layout-components
    • duck-design-web-components

    But it does give each team the flexibility to contribute beyond their core area and not be blocked by other teams.

    Let me know what you think or how you'd improve it. Do you already use an approach like this?

    Development, Opinion

  • 7 May 2020

    One product, many owners

    or: "What you are is not what you do"

    Imagine you're on a bank heist. You're literally right in the middle of the job. Your expert team consists of some very talented criminals. There's The Hacker, The Driver, The Money Man, The Explosives Expert, You.

    Heist

    While The Hacker is rewriting the safe door control circuit's firmware to speed up the clock and release the time-lock, you find the computer controlling the security cameras. Do you make a note on a ToDo list?

    • Remind the Hacker to delete the security footage [ ]

    Or do you grab the mouse and select File > Delete Security Footage?

    (Yes, it is that easy, you specifically chose this bank to hit because it has terrible security)

    Once you leave the building, there's a bit of confusion and The Explosives Expert ends up in the driving seat. The Driver is in the back! The police are one block away. Do you all get out, rearrange, let the driver get in the front? Or do you just start driving? The Explosives Expert may not know the exact route but, remember, The Driver is right there, able to give directions.

    And that is literally exactly what developing a software product is like.


    Product

    The role of the Product Owner is fairly well known, well defined and ubiquitous across agile teams. But we know that everybody in the team is responsible for the product. The PO might be an expert in certain areas of understanding the customers' mindset or prioritisation but everyone is responsible for the product. Everyone owns the product but, in this instance, there is one person with the title Product Owner.

    Quality

    In the same way, everyone working on a product is responsible for the quality. If you're working on the database and you find that the drop-down menu occasionally disappears, there are multiple ways you could deal with it.

    • You could just ignore it. It's not the databases's fault, after all.
    • You could file a ticket in the system for someone else to pick up.
    • You could figure out a reproducible test-case.
    • You could pop open the web inspector and check the console, poke around a bit.
    • You could check out the code and fix it.

    There are a lot of ways you could do this, depending on your abilities and your available time but whatever you do – as long as it's not just ignoring the problem – you are taking a step to improve the quality of the product.

    The QA might be an expert in writing test cases or have the kind of brain that would think of using a base64-encoded png as a username but that doesn't mean quality begins and ends with them. They are the Quality Owner. Everyone is responsible for quality, but it's the Quality Owner's priority.

    Architecture

    And so we come to the Architecture Owner. Even now, a couple of decades after the Agile Manifesto with repeated swings for and against it, Agile and Architecture have an uneasy relationship or, at least, a poorly-defined one.

    The image of The Architect as the keeper of the book, the writer of the specification still triggers rebellion in the minds of those who zealously misunderstand agile practices. Of course, there can't be a predestined development plan when the destination is unknown. But that doesn't mean you blindly run headlong into every problem. You look around, you keep aware of your surroundings, of other projects. If nothing else, you stay aware of your situation to know if you're doing something you've done before. If you are, you can look at how you solved this in the past and do the same or better this time round. This is everyone's responsibility but it's also Architecture.

    "The last time I did a job in this town, the bank had a silent alarm system. You might want to cut the wires on this one before it's triggered this time."

    Agile architecture is about looking a little bit forward and a little bit back. How will this work next year? How did we solve it last year? Is another team working on the same thing? Did they? Will they? Any prototyping done to figure out the next step of the journey is architecture.

    Just as development, quality, product, design, business are all essential parts of any project, so is architecture. The Architecture Owner doesn't only do architecture and isn't the only person to do architecture tasks. It is merely their priority. They may have additional expertise or experience, they may be faster at building prototypes or drawing boxes-and-arrows but they are as integral a part of the product development team as anyone else.

    To be effectively agile, everyone should care ultimately about creating the product.


    Practicality

    This is all good in theory. What does this actually mean? Is 'Architecture Owner' a full-time job? Does an architect architect all day long?

    Well, is Product Owner a full-time job? QA? Explosives Expert? For a lot of teams, it is. But not all. A particularly small team might have an Engineer who acts as part-time PO. A larger team will have a dedicated person for each role. What you are is not what you do. The whole point of this is that being the owner of a domain doesn't make you solely and exclusively responsible.

    In many cases, the person who assumes the AO role will be a strong engineer who can jump on any part of the codebase and be effective quickly (because in addition to being experienced engineers, they understand the architecture and the motivations behind the architecture) and who coaches others to think more architecturally and be effective quickly.


    Notes

    In the decade or so since I first wrote this, the role of architecture in agile has come and gone and come back a couple of times. I find myself directing people to the same articles repeatedly so I figured it was time to update and publish this.

    There are many other articles on agile architecture, most of which are written much more seriously than this.

    • https://www.ben-morris.com/relax-theres-no-conflict-between-architecture-and-agile/
    • https://www.adventureswithagile.com/2015/01/19/architecting-an-agile-solution-with-help-from-dad/
    • http://www.agilemodeling.com/essays/architectureOwner.htm
    • https://en.wikipedia.org/wiki/Agile_Architecture

    Geek, Development, Opinion

  • 19 Jul 2019

    Web Components vs Vue Components

    I've been doing a lot of work in Vue recently so when I was asked to evaluate using Web Components on an upcoming project, I approached it with a Vue-ish mindset.

    I've not really kept my eye on Web Components for the last couple of years beyond seeing original proposals being superceded and import specs being replaced. Just seeing those things on the periphery were enough to make me think "Meh... I'll have a look later when it's all died down".

    Now, I know that Web Components !== Vue. But, I was interested in what knowledge could be migrated from one technology to the other. If I were building an actual web app, I'd definitely use Vue. Building a boxful of reusable, shareable UI elements, though... let's find out.


    I'm not going to build anything too complex to start with. How about an "Planet Summary" panel? A simple panel that renders summary information about a planet given a JSON object.

    I have an API that returns JSON information about where in the sky to find planets when given your latitude and longitude. For example, if you're standing slightly south of the centre of Berlin and want to know where Venus is, you'd make this request:

    https://planets-api.awsm.st/venus/52.5/13.4

    And the response would be:

    
    {
      "name": "Venus",
      "number": 1,
      "colour": 1,
      "colleft": 24,
      "colright": 25,
      "alt": 13.043427032890424,
      "az": 290.3495756869397,
      "dec": 22.661411404345362,
      "ra": 110.21545618074397,
      "H": 98.18491228623316,
      "eclon": 108.59563862950628,
      "eclat": 0.5200939814134588,
      "illum": 0.9918628383385676,
      "r": 0.7192422869900328,
      "dist": 1.7155717469739922,
      "mag": -3.909377586961354,
      "elong": 0,
      "pa": 0,
      "p": 1,
      "description": {
        "altitude": "Barely above the horizon",
        "azimuth": "West"
      },
      "visible": false
    }
    

    In this case, it determines Venus isn't visible because, even though it's above the horizon, it's not bright enough given the time of day (about 6pm).

    We want to make a little UI card that displays this information.

    Planet panel

    Mapping Vue features to Web Components

    VueWeb ComponentNotes
    nameclass name
    datainstance properties
    propsattributesthese are not reactive by default. Attributes have to be specifically observed (see watch).
    watchattributeChangedCallbackfirst, register your watched attributes with `observedAttributes` then process them in attributeChangedCallback
    computedgetters
    methodsclass methods
    mountedconnectedCallbackcalled async so the component may not be fully ready or may have been detached. Use Node.isConnected to protect against calling a dead node
    componentWillUnmountdisconnectedCallback
    style blockstyle block inside templatestyles are scoped by default
    template blockliteral templateJS literal templates (backtick strings) are nowhere near as powerful for templating as an actual template library. Vue template features such as `v-for` can be replicated with vanilla JS but a single-purpose template library (such as `lit-html`) is a good idea.

    NOTE: I am deliberately not using Webpack. I realise that actual applications would be using additional tooling but I want to see what we can do without it.


    The first thing that clicked with me was when I realised that computed properties and getters are identical. Nice.

    Here's Vue code to return the planet name or a default string:

    
    computed: {
      name() {
        return this.planet.name || '';
      },
    }
    

    And Web Component:

    
    get name() {
      return this.planet.name || '';
    }
    

    Well, that was easy (and trivial).

    The same goes for defining the custom element for use in the DOM

    Vue:

    
    components: { 
      "planet-summary": PlanetSummary
    }
    

    Web Components:

    
    customElements.define("planet-summary", PlanetSummary);
    

    The only real difference at this level is the data binding. In Vue, props passed from a parent element to a child are automatically updated. If you change the data passed in, the child updates by default. With Web Components, you need to explicitly say you want to be notified of changes.

    This is basically the same as setting a watch in Vue. Data that changes in a slightly less tightly-bound fashion can be watched and the changes trigger updates further down.

    Watches

    Watches in Vue:

    
    watch: {
      altitude(newValue, oldValue) {
        ...
      }
    }
    

    With Web Components, registering a watch and reacting to changes are separate:

    
    static get observedAttributes() {
      return ['altitude'];
    }
    
    attributeChangedCallback(name, oldValue, newValue) {
      if(name === 'altitude') {
        ...
      }
    }
    

    Templating

    Vue contains full templating support – for loops, conditional rendering, seamless passing around of data. Natively, you have literal templates and that's about it.

    To create a list of planets, you'd use the v-for directive and loop over your planets array.

    Vue:

    
    <ul>
      <li v-for="planet in planets">
        <planet-summary :planet="planet"></planet-summary>
      </li>
    </ul>
    

    Web Component

    
    <ul>
      ${this.planets.map(planet => `
      <li>
        <planet-summary planet='${JSON.stringify(planet)}'></planet-summary>
      </li>
      `).join('')}
    </ul>
    

    The join is there because we're creating an HTML string out of an array of list items. You could also accomplish this with a reduce.

    Boilerplate

    With Web Components, your component lives in the Shadow DOM so you are responsible for updating it yourself. Vue handles DOM updates for you.

    Here is a basic render setup:

    
      constructor() {
        super();
        this._shadowRoot = this.attachShadow({ mode: "open" });
        this.render();
      }
      render() {
        this._shadowRoot.innerHTML = '';
        this._shadowRoot.appendChild(this.template().content.cloneNode(true));
      }
    

    This needs to be explicitly included in every component as they are standalone whereas Vue automatically handles DOM updates.

    CSS

    Due to the fact that Web Components live in a separate document fragment, There are complications around sharing styles between the host page and the component which are nicely explained on CSS Tricks. The biggest benefit, on the other hand, is that all styles are scoped by default.

    Vue without Webpack (or other tooling) also has its own complications around styles (specifically scoping styles) but if you're building a Vue application, it is much more straightforward to specify which styles are global and which are scoped.

    Summary

    Here is the Vue Planet Summary and the source of planet-summary-vue.js.

    Here is the Web Component Planet Summary and the source of planet-summary.js.

    Bonus: here's a Planet List Web Component which includes the Planet Summary component. And the source of planet-list.js

    All in all, pretty much everything between basic Vue Components and Web Components can be mapped one-to-one. The differences are all the stuff around the basic construction of the components.

    I'd say that if you're looking to build completely standalone, framework-free reusable components, you'll be able to accomplish it with the Web Components standard. You just might have a bit of extra lifting and boilerplate to deal with.

    On the other hand, if you're already planning on building a full web application with data management and reactive components, use the tools available to you.

    Geek, Development, CSS, Javascript

  • newer posts
  • older posts

Categories

Toys, Guides, Opinion, Geek, Non-geek, Development, Design, CSS, JS, Open-source Ideas, Cartoons, Photos

Shop

Colourful clothes for colourful kids

I'm currently reading

Projects

  • Awsm Street – Kid's clothing
  • Stickture
  • Explanating
  • Open Source Snacks
  • My life in sans-serif
  • My life in monospace
Simon Madine (thingsinjars)

@thingsinjars.com

Hi, I’m Simon Madine and I make music, write books and code.

I’m the Engineering Lead for komment.

© 2025 Simon Madine