0.0.1-alpha.306 • Published 1 year ago

@incremental.design/component-base v0.0.1-alpha.306

Weekly downloads
-
License
MIT
Repository
-
Last release
1 year ago

incremental.design/component-base

Build your own user interface (UI) component libraries 10x faster.

So, you're making a web app. You have a few dozen user flows, and a design system with hundreds of styles, elements and patterns. Now it's time to make the app real. But, before you can assemble any user flows, you need to turn the design system into code you can actually use. In other words, you need to build a UI component library. The base component makes this quick and easy. It handles appearance and interactivity for you, by updating its CSS in response to user interactions. When you start with the base component, you don't have rewrite variations on the same event handlers, finite state machines, and CSS selectors for every component in your library. Use the base component to make your library in 10% of the time, so you can spend the other 90% telling everyone how fast you are.

  • Respond to user interactions - without writing event handlers - by wrapping your component's markup in the base component's default slot.

    Use the base component's props to add affordances to your component's markup.

    Whenever you click, tap, press, or otherwise interact with your component, the base component will emit a stateChange custom event according to the props you supplied. All you need to do wrap your component's markup in the base component's default slot and use @stateChange to run your component's methods.

  • Customize the base component's theme with a single string.

    Use the `:theme` prop to customize the styles contained in the default slot's `Theme` slot prop

    Pass a string or PlatformInterface object into the base component's :theme prop to customize the styles that it will provide to your component. You choose the theme, and the base component turns it into CSS for you.

  • Theme your component, without writing a single CSS selector, by adding the base component's styles to your component.

    Use the functions in the `Theme` slot prop to style your component's markup

    The base component's Theme slot prop contains all of the CSS you need to style your component. All you need to do reach into it, grab the styles you need, and bind them to your markup's :style attributes. It will even update your component's appearance in response to user interactions - no :hover or :active selectors required!

  • Show a placeholder while your component loads, without writing a single line of code.

    Use the `<template v-slot:"fallback">` to customize your component's placeholder

    The base component implements the Vue 3 suspense API, so you don't have to. If your component loads asynchronously, the base component will automatically show a blockframe while it's loading. If you'd like to customize the blockframe, all you need to do is insert your own markup into the base component's fallback slot.

Installation:

  1. Add the incremental.design/component-base package to your project:

  2. Import component-base into any of your Vue 3 components as follows:

    <script lang="ts">
    import BaseComponent from `@incremental.design/vue3-component-base`;
    
    export default defineComponent({
      components: {
        BaseComponent,
      },
    });
    </script>
  3. Add BaseComponent to your Vue 3 component's template:

Usage:

Unlike a website, a web app demands a high level of interactivity. You can't just apply styles to HTML markup. You have to split your markup into reusable pieces, and give each of them affordances: user interactions to which they respond. The base component makes this automatic. You choose the affordances and the base component applies them to your markup. It does this by translating user interactions into changes in your markup's state and appearance.

  • A state is a set of rules that determine how user interaction affects your markup.

    For example, if your markup is in a 'not pressed' state, then any click or tap will transition it into a 'pressed' state. On the other hand, if your markup is already in a 'pressed' state, then the same clicks and taps will have no effect.

  • Appearance is a visual indication of state. When state changes, appearance changes accordingly.

    For example, if your markup transitions from a 'not pressed' state to a 'pressed' state, then its fill, border, shadow and text color will change.

All UI components need to have at least a few states in order to respond to user interaction. Without the base component, you would have to write the code that turns this user interaction into states and appearances by yourself.

Toggle the base component's affordances:

What's the difference between a button, a switch and a field? If you answered, "their affordances", you're right! The UI components in your library almost certainly differ in their affordances. It's up to you to choose the right ones for each of them. This can get complicated, because affordances are dependent: it's impossible to have certain affordances without others. For example, a UI component can't be pressed if it can't be hovered and it can't be toggled if it can't be pressed. Before you can choose affordances for your component, you have to learn what they do, and how they depend on each other.

For the most part, any UI component can have some, or all of the following affordances:

Notice that most of these affordances depend on the 'Pressable' affordance, and all of them depend on the 'Hoverable' affordance. You can't make a UI component toggleable if it isn't pressable. You can't make it pressable if it isn't hoverable.

All affordances depend on the `Hoverable` affordance, and most depend on the `Pressable` affordance

If you're thinking "ok, all of this is super, but which affordances do I need for my component?", here are a few examples:

Use the following props to tell the base component what affordances it should add to your UI component:

Although most affordances depend on others, you have to manually specify each affordance you want to add to your UI component. For example:

  • If you want to make your UI component focusable, you not only need to add the isFocusable prop, but also the isPressable and isHoverable prop to the base component.

This is by design. Each prop is a flag: its presence enables the corresponding affordance. Its absence disables it. If you omit a dependent affordance, the base component will still handle its corresponding user interactions. It just won't update your markup's state or appearance in response. While this isn't usually the behavior you want, sometimes it can be very useful. For example:

  • You add the isSlideable prop to the base component, but omit the isPressable and isHoverable props, because you want the contents of your UI component to follow your mouse or fingertip when you press it, but you don't want the UI component to change its appearance when it is hovered or pressed.

  • You add the isToggleable prop to the base component, but omit the isPressable and isHoverable props, because you want your UI component to change its appearance when you toggle it, even as it maintains the same appearance when you hover on or press it.

Wrap your component's template in the base component's default slot:

Once you choose the affordances you want your UI component to have, it's up to the base component to apply them. It can't do that until you insert your component's markup into the base component's default slot. If you haven't used Vue slots, think of them as a way to swap out the base component's markup, without swapping out its script. In other words, you're changing the way the base component looks, without changing what it actually does.

The base component receives props and user interactions, and returns slot props.

The base component uses Vue custom events and slot props to apply affordances to your markup. It supplies you with several of these values, each of which change whenever the base component handles a user interaction.

Use the stateChange custom event to run methods when your component's state changes.

You probably want your components to affect your app. For example, when a it transitions from a 'not pressed' to a 'pressed' state, you probably want it to do something. Maybe you want it to show or hide a menu, apply a setting, or submit user input. Although the base component will update your component's state and appearance, it won't handle any business logic. That's up to you. To make your component useful, you need to run your business logic when your component's state changes. To do this, wrap your logic in a method, and use the base component's stateChange event to trigger it:

  1. Define the method you want to run each time the base component emits a stateChange event. In this example, that's the doSomething method:

    Make sure your method accepts the following arguments:

  2. Bind the stateChange event to the method you just defined.

    Whenever the base component emits a stateChange event, it will pass the newState, oldState and input to your method.

Once you set up this method, you can conditionally execute business logic, depending on the value of currentState, oldState and inputEvents. Most of the time, you'll only need currentState, but for more advanced behaviors, you might want the oldState and inputEvents as well. Keep in mind that all of these arguments are arrays. This is by design. It makes compound states and inputs - states that are a union of two or more states, and inputs that are a union of two or more inputs, respectively - easy to parse. For example:

Compound StateHow it's represented in the currentState and previousState arrays:
Pressed and Selected['pressed', 'selected']
Hovered and Peeked['hovered', 'peeked']

These arguments can also be empty arrays. Once again, this is by design. The presence of a BaseComponent.state indicates that the component is in that state. The absence indicates that the component is not in that state. For example:

StatenewState or oldState array:
Not Hovered, Pressed, Selected['pressed', 'selected']
Hovered, Not Pressed, Not Selected['hovered']
Not Hovered, Not Pressed, Not Selected[]

There is exactly one BaseComponent.state for each affordance. Of these, your component can only have the ones that correspond to the affordances you chose for it. In other words, a state can only appear in the newState and oldState arrays if you specifically add the prop for the corresponding affordance to the base component:

AffordancePropCorresponding BaseComponent.state:
HoverableisHoverablehovered
PeekableisPeekablepeeked
PressableisPressablepressed
ToggleableisToggleabletoggled
SlideableisSlideablesliding
SelectableisSelectableselected
FocusableisFocusablefocused

To conditionally execute your business logic, all you need to do is add a switch() statement to the method you bound to the base component's stateChange event. This switch statement should contain a case for each of your component's possible BaseComponent.states.

The possible values of the inputEvents array are similarly limited. Although they are objects with nested properties, rather than strings, they all conform to the EventInfo type. This means that they contain a type, timestamp, and input property. The input property will always be an object of type DragInput, DeviceInput, GamepadInput, ScrollInput, FocusInput, KeyboardInput, or PointerInput. Each of these types contains all sorts of useful information about the user interaction that triggered the state change. While you don't need to use the values of input to trigger your component's business logic, you can increase your component's level of interactivity by incorporating them into it.

Use the pointerInput, focusInput, keyboardInput, dragInput, and scrollInput custom events to respond to every user interaction, all the time.

If you want to take your UI components to the next level, you can't just trigger business logic on state changes. That's because most user interaction is continuous - not discrete. And although the base component reduces continuous interaction into discrete state changes, your components need to show users that they're always listening. They need to respond to everything a user does - every mouse movement, every touch, every scroll, every keypress - regardless of whether it triggers a state change. The base component makes this trivial. It already handles every user interaction your component receives. Even better, it translates each of them into an EventInfo object. Just like the EventInfo objects contained within the stateChange event's input array, these objects contain a type, timestamp, and input field:

The base component emits five distinct custom events: one for each type of input. This makes it effortless to filter the interactions that matter to you. If you only want to respond to keyboard interactions, just bind a method to the keyboardInput event. If you only want to respond to mouse and touch events, just bind a method to the pointerInput event. Each of the following five events passes a single EventInfo object into the method you bind to them.

You can, of course, listen to any combination, or all of these events if you'd like. When you listen to any of these custom events, you don't have to listen to the native events they replace:

Set the base component's theme with the theme prop:

Your app's design system likely prescribes a theme: a set of styles all of your components adopt. Part of making UI components is turning those styles into code. If CSS is your native language, this might sound easy enough. But keep in mind that UI components have to respond to every user interaction. CSS alone won't cut it. You need to drive your CSS with Vue. The base component takes care of this for you. You tell it what kind of theme you want to apply, and it drives ALL of the styles your component needs. Without the base component, you would have to co-mingle your presentation logic with your business logic to affect both your component's styles and your app. The base component moves the presentation logic out of your component.

The base component ships with six preset themes, each of which you can set using the theme prop:

Keep in mind that props are reactive: changing the value of the prop changes the theme on the fly. This can come in handy when you want to demonstrate the same component with different themes applied to it. In fact, this is the strongest use case for the base component.

If none of the six preset themes fit your design system, you can easily define your own themes.

if you don't specify the theme prop, then the base component won't calculate styles for you. This is a great option if you've already written code for your app's theme, and you just want to use the base component's states. In fact, if you don't supply a theme prop, you will actually get a slight performance boost, because the base component won't calculate any of the theme's styles.

Style your component with the base component's layout slot prop:

You just set the base component's :theme, and ... nothing changes. Your component's appearance hasn't updated to reflect the theme. This is a feature, not a bug. Although the base component calculates and updates the styles in a theme, you have to manually apply the styles you need to your markup. That's because the base component provides you with several variations on the same theme. It's up to you to apply the one that fits your component. To do this, you need to:

  1. Choose any of the options in layouts and insert it into layout:

  2. Call style with the name of the style you want to apply.

  3. If you want to force a specific tint, you can do so by inserting the tint you want into style:

    <pre>
    <code>
    &lt;template&gt;
      &lt;BaseComponent :theme="ios" isPressable isHoverable&gt;<br/>
        &lt;template v-slot:default="{ <strong>layout</strong>, <strong>layouts</strong> }"&gt;<br/>
          &lt;div :style="layouts('inline').style('bgInline',<strong>'active'</strong>)"&gt;&lt;/div&gt;
            &lt;!-- in this case, layouts[0] === 'inline' --&gt;<br/>
        &lt;/template&gt;<br/>
      &lt;/BaseComponent&gt;
    &lt;/template&gt;<br/>
    &lt;script&gt;<br/>
      import { defineComponent } from 'vue';
      import BaseComponent from '@incremental.design/vue3-component-base';<br/>
      export default defineComponent({<br/>
        /* your component's <a href="https://v3.vuejs.org/api/options-api.html" target="_blank">options</a> here */<br/>
      })<br/>
    &lt;/script&gt;
    </code>
  4. If you want to force dark mode, you can do so by setting the darkMode argument of style to true:

    The layouts array contains all of the options that layout accepts. The contents of layouts vary based on the theme you choose.

    The layout function will tell you what styles, tints and color modes the style function accepts, if you destructure its return object as follows:

The specific styles available will vary based on the layout you choose. For more information on all of the available styles, see the README for @incremental.design/theme.

Display a custom placeholder while your component is loading:

How incremental.design/vue3-component-base works:

The base component listens for HTML events, updates state, and sends the state into the scope of both the default slot and the suspense slot

Explain how execution works. What is the entry point for your code? Which files correspond to which functionality? What is the lifecycle of your project? Are there any singletons, side effects or shared state among instances of your project? Take extra care to explain design decisions. After all, you wrote an ENTIRE codebase around your opinions. Make sure that the people using it understand them.

Repository Structure:

0.0.1-alpha.420

1 year ago

0.0.1-alpha.421

1 year ago

0.0.1-alpha.425

1 year ago

0.0.1-alpha.422

1 year ago

0.0.1-alpha.423

1 year ago

0.0.1-alpha.428

1 year ago

0.0.1-alpha.429

1 year ago

0.0.1-alpha.426

1 year ago

0.0.1-alpha.427

1 year ago

0.0.1-alpha.413

1 year ago

0.0.1-alpha.411

1 year ago

0.0.1-alpha.412

1 year ago

0.0.0

1 year ago

0.0.1-alpha.417

1 year ago

0.0.1-alpha.418

1 year ago

0.0.1-alpha.415

1 year ago

0.0.1-alpha.416

1 year ago

0.0.1-alpha.387

2 years ago

0.0.1-alpha.388

2 years ago

0.0.1-alpha.380

2 years ago

0.0.1-alpha.399

2 years ago

0.0.1-alpha.391

2 years ago

0.0.1-alpha.394

2 years ago

0.0.1-alpha.395

2 years ago

0.0.1-alpha.365

2 years ago

0.0.1-alpha.366

2 years ago

0.0.1-alpha.363

2 years ago

0.0.1-alpha.364

2 years ago

0.0.1-alpha.403

2 years ago

0.0.1-alpha.400

2 years ago

0.0.1-alpha.367

2 years ago

0.0.1-alpha.376

2 years ago

0.0.1-alpha.377

2 years ago

0.0.1-alpha.374

2 years ago

0.0.1-alpha.375

2 years ago

0.0.1-alpha.378

2 years ago

0.0.1-alpha.379

2 years ago

0.0.1-alpha.373

2 years ago

0.0.1-alpha.362

2 years ago

0.0.1-alpha.343

2 years ago

0.0.1-alpha.341

2 years ago

0.0.1-alpha.342

2 years ago

0.0.1-alpha.325

2 years ago

0.0.1-alpha.326

2 years ago

0.0.1-alpha.340

2 years ago

0.0.1-alpha.329

2 years ago

0.0.1-alpha.327

2 years ago

0.0.1-alpha.328

2 years ago

0.0.1-alpha.332

2 years ago

0.0.1-alpha.333

2 years ago

0.0.1-alpha.330

2 years ago

0.0.1-alpha.331

2 years ago

0.0.1-alpha.336

2 years ago

0.0.1-alpha.337

2 years ago

0.0.1-alpha.334

2 years ago

0.0.1-alpha.335

2 years ago

0.0.1-oops.327

2 years ago

0.0.1

2 years ago

0.0.1-alpha.338

2 years ago

0.0.1-alpha.339

2 years ago

0.0.1-alpha.322

2 years ago

0.0.1-alpha.315

2 years ago

0.0.1-alpha.323

2 years ago

0.0.1-alpha.313

2 years ago

0.0.1-alpha.319

2 years ago

0.0.1-alpha.316

2 years ago

0.0.1-alpha.314

2 years ago

0.0.1-alpha.312

2 years ago

0.0.1-alpha.311

2 years ago

0.0.1-alpha.310

2 years ago

0.0.1-alpha.309

2 years ago

0.0.1-alpha.308

2 years ago

0.0.1-alpha.307

2 years ago

0.0.1-alpha.306

2 years ago