npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@berglund/mixins

v0.0.14

Published

TypeScript mixins for Angular components.

Downloads

31

Readme

Mixins

@berglund/mixins is a collection of TypeScript mixins. The mixins form a layer between apps and component libraries that aims to

  • increase productivity
  • reduce code duplication
  • reduce developer freedom

Before getting into how, let's get into the upsides and downsides of traditional Angular component libraries.

Traditional component libraries

The upsides

A well-written Angular component library usually has:

  • lots of content projection
  • lots of directives

And you can see why, it is a very strong combination. The content projection maximizes component surface area, which the directives can attach to.

Take mat-select for example:

<mat-form-field>
  <mat-label></mat-label>
  <mat-select>
    <mat-option></mat-option>
  </mat-select>
</mat-form-field>

Since the component has a large surface area, a cdkDrag directive could make the options rearrangeable. And an *ngFor directive could repeat <mat-option> over some data source. It is a powerful design, but the power comes at a cost.

The downsides

Let's look at another material component, a basic mat-table. At this point, its implementation is pretty simple, with some 50 odd lines of code. But tables usually have many requirements, such as

  • sortable rows
  • virtualized rows
  • rearrangeable columns

Let's add these features to mat-table using directives from @angular/material and @angular/cdk

| Feature | Directives | | --------------------- | :---------------------------------------------: | | Sortable rows | matSort, matSortHeader | | Virtualized rows | cdk-virtual-scroll-viewport, *cdkVirtualFor | | Rearrangeable columns | cdkDrag, cdkDropList |

Pretty smooth, but now the table is now hundreds of lines of code. And here lies the issue of traditional component libraries: large complicated templates. The problem with these templates is that they lead to

  • code duplication
    • you either have to duplicate the template code in future tables...
    • ...or you have to create a table-wrapper component that propagates inputs
  • few constraints
    • a developer can attach directives, but should they? In enterprise, large freedom can make UX diverge across apps
  • technical bias
    • the code is overly focused on how to solve a problem, not what it's trying to solve. Let's say mat-table suddenly needs nested drag drop. Since @angular/cdk/drag-drop does not support that, the code needs massive refactoring
  • poor dynamic support
    • neither Angular directives or @ContentChildren can attach dynamically
  • poor serialization
    • since the API is partly described by templates, components cannot be serialized. This makes mapping models, such as JsonSchema, to components very difficult.

The solution

Let's add another layer between apps and libraries. The goal is a programmatic API that and fully described by its inputs, and not at all by the template.

To achieve that, all content projection has to go. But without content projection, the layer loses a lot of reusability. It can no longer can delegate functionality to directives. Instead, it has to to describe all functionality in its API.

Mixins

To find another source of reusability, let's look at four components

  • mat-checkbox
  • mat-select
  • matInput
  • mat-table

and find their commonalities

| Feature | Component | | -------------------- | :--------------------------------------: | | Can have a label | mat-checkbox, mat-select, matInput | | Can show data | mat-select, mat-table | | Can select | mat-select | | Can have state | mat-checkbox, mat-select, matInput | | Can render templates | matTable | | Can be disabled | mat-checkbox, mat-select, matInput |

and implement classes for each of the listed features. Then, use TypeScript mixins to compose a base. There are existing bases for components in @berglund/mixins, but let's create a couple of new ones.

const TableBase = mixinComponentOutlet(mixinCollection(_TableBase));
const SelectBase = mixinConnectable(
  mixinAccessible(mixinLabel(mixinSelection(mixinCollection(_SelectBase))))
);

The base is then ready to be used in a component.

@Component({
  templateUrl: './select.component.html',
  styleUrls: ['./select.component.scss'],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SelectComponent extends SelectBase {}

Here, the template in select.component.html implements the mixin API using a design system. The app will now uses the mixin API over the previous API.

This is how a select would look like using @berglund/material

// select-mixin.component.ts
import {
  ChangeDetectionStrategy,
  Component,
  ViewEncapsulation,
} from '@angular/core';

@Component({
  templateUrl: './select-mixin.component.html',
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SelectMixinExampleComponent {}
<!-- select-mixin.component.html -->
<berg-select label="Drinks" [data]="['Coffee', 'Tea']"></berg-select>
// select-mixin.module.ts
import { NgModule } from '@angular/core';
import { BergSelectModule } from '@berglund/material';
import { SelectMixinExampleComponent } from './select-mixin.component';

@NgModule({
  declarations: [SelectMixinExampleComponent],
  exports: [SelectMixinExampleComponent],
  imports: [BergSelectModule],
})
export class SelectMixinExampleModule {}

Or if you wanted to increase reusability and declare the inputs programmatically

// select-mixin-programmatic.component.ts
import {
  ChangeDetectionStrategy,
  Component,
  ViewEncapsulation,
} from '@angular/core';
import { BergSelectComponent } from '@berglund/material';
import { component } from '@berglund/mixins';
import { of } from 'rxjs';

@Component({
  templateUrl: './select-mixin-programmatic.component.html',
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SelectMixinProgrammaticExampleComponent {
  drinks = component({
    component: BergSelectComponent,
    inputs: {
      label: 'Drinks',
      data: ['Coffee', 'Tea'],
    },
  });

  eveningDrinks = component(this.drinks, { data: of(['Beer', 'Wine']) });
}
<!-- select-mixin-programmatic.component.html -->
<berg-outlet [component]="drinks"></berg-outlet>
<berg-outlet [component]="eveningDrinks"></berg-outlet>
// select-mixin-programmatic.module.ts
import { NgModule } from '@angular/core';
import { BergOutletModule } from '@berglund/mixins';
import { BergSelectModule } from '@berglund/material';
import { SelectMixinProgrammaticExampleComponent } from './select-mixin-programmatic.component';

@NgModule({
  declarations: [SelectMixinProgrammaticExampleComponent],
  exports: [SelectMixinProgrammaticExampleComponent],
  imports: [BergOutletModule, BergSelectModule],
})
export class SelectMixinProgrammaticExampleModule {}

As you can see, the API is

  • + intent-focused
  • + reusable, components are easily reused since they are described as objects
  • - stiff. You cannot even add a (click)-binding. Everything has to be described in the mixin-API

So when is this useful?

Is a layer between apps and component libraries is a good idea for your code? It depends on the context. If you're working on a hobby-project, the constraints would probably be too frustrating. But if you're working in a company with multiple apps, then a layer using mixins would do a lot for productivity and unified UX. Not to mention that serializable components is a game changer for apps with forms that need customization.