• Stars
    star
    494
  • Rank 87,025 (Top 2 %)
  • Language
    JavaScript
  • License
    BSD 3-Clause "New...
  • Created about 7 years ago
  • Updated over 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A Higher Order Component for Preact that resolves (async) values from a model and passes them down as props.

 Wiretie
npm travis

Wiretie is a Higher Order Component for Preact that resolves (async) values from a model and passes them down as props. It lets you wire() components up to data sources.

This provides a uniform and streamlined way to write async components that avoids complex side effects within componentDidMount(), and encourages proper instantiability.

Here's what it does for your code:

Example source code: before & after

Features

  • Standalone and library-agnostic, works with any model
  • Uses the component hierarchy to avoid singletons and duplication
  • Automatically re-renders your component with resolved data
  • Maps props to model methods, with optional transformation
  • Provides Promise status as pending and rejected props
  • Intelligently reinvokes methods when mapped prop values change
  • Replaces tricky side effecting componentDidMount() methods
  • Safely abstracts access to context


Overview

At a high level, the process for working with wire() works like this:

  1. Write a model:
    • Make sure it's a factory, with parameters for things like configuration
    • Make sure the returned methods have logical inputs and outputs
    • Write tests for it, perhaps publish it to npm
  2. Write a pure "view" component:
    • Data should be plain objects passed in as props
    • It doesn't have to be a function, but it should work like one
    • Use state where appropriate: for controlling the view
    • Use the pending and rejected properties to respond to unresolved promises and error conditions (info)
  3. Instantiate & expose the model:
    • Invoke the model (factory) with any options
    • Store the instance as a Component property or a global
    • Expose it into context using a <Provider>
  4. wire() the view up to the model
    • wire(name) connects to context[name] (<Provider name={..}>)
    • The 2nd argument (mapToProps) is the "wiring"
      • Keys are the prop names to pass to the view, values are functions to call on the model
      • Pass args to model functions: prop: ['foo', {}]
      • 💭 a:['b','c'] is like a = await b('c')
    • 3rd argument (mapModelToProps) lets you map your model instance to view props
      • Useful for mapping model methods to event handler props
  5. That's it!

Handling Loading & Errors

In addition to passing mapped data down as props, wiretie also passes special pending and rejected props.

If any promises are still being waited on, the prop names they are mapped to will be keys in a pending object. Similarly, if any promises have been rejected, their corresponding prop will be a key in rejected with a value matching rejection value of the promise.

The pending and rejected props are undefined if there are no promises in that state. This means if you only need to know if anything is loading, you can just check if (props.pending).

The following example shows states for two properties:

const Demo = wire(null, {
    foo: Promise.resolve('✅'),
    bar: Promise.reject('⚠️')
})( props => {
    console.log(props);
})

render(<Demo />)

// logs:
  { pending: { foo: true, bar: true } }
// ... then:
  { foo: '✅', rejected: { bar: '⚠️' } }

Use pending to show a "loading" UI for some or all props.

💥 Use rejected to respond to per-prop or overall error states.


Usage

The signature for wire() consists of three arguments, all of which are optional (they can be null).

wire(
    // the property in context where a model instance exists
    <String> contextNamespace,
    // maps incoming props to model method call descriptors
    <Function|Object> mapToProps,
    // maps model properties/methods to props
    <Function> mapModelToProps
)

See Full wire() documentation for parameter details.

🤓 Want to dive straight in? Start with this Wiretie Codepen Example.

Usage With Functional Components

Simple connected view

const Username = wire('user', {
    // pass resolved value from user.getUsername() down as a "username" prop:
    username: 'getUsername'
})( props =>
    <span>
        {props.username}
    </span>
))

💁 Note: for the first render, props.username will be undefined.

The component will re-render when getUsername() resolves, passing the value down as props.username.

Handling pending state

Let's show an indicator while waiting for username to resolve:

const Username = wire('user', {
    username: 'getUsername'
})( props =>
    <span>
        { props.pending ? 'Loading...' : props.username }
    </span>
))

Splitting things apart

// A pure "view" component:
const Username = props => (
    <span>
        { props.pending ? (
            // display a spinner if we are loading
            <Spinner />
        ) : props.rejected ? (
            // display error message if necessary
            `Error: ${props.rejected.username}`
        ) : (
            // display our data when we have it
            props.username
        }
    </span>
);

// bind the "view" to the model:
const MyUsername = wire('user', {
    username: 'getUsername'
})(Username)

💁 Notice we've added error handling to the example.

Usage with Classful Components

@wire('user', { username: 'getUsername' })
class Username extends Component {
    render(props) {
        return (
            <span>
                { props.pending ? (
                    // display a spinner if we are loading
                    <Spinner />
                ) : props.rejected ? (
                    // display error message if necessary
                    `Error: ${props.rejected.username}`
                ) : (
                    // display our data when we have it
                    props.username
                }
            </span>
        );
    }
}

Event handlers for mutation

const mapUserToProps = user => ({
    onChange(e) {
        user.setUsername(e.target.value)
    }
})
@wire('user', { username: 'getUsername' }, mapUserToProps)
class Username extends Component {
    render({ username, onChange }) {
        return <input value={username} onInput={onChange} />
    }
}

Thinking in MVC / MVVM?

Let's see the example rewritten using that terminology:

const View = props => (
    <span>
        {props.username}
    </span>
);

const viewModel = wire('user', { username: 'getUsername' });

const Controller = viewModel(View)

render(
    <Provider user={new UserModel()}>
        <Controller />
    </Provider>
)

Tutorial

A "Hardware" Model

We're going to build a model that provides access to some computer hardware, in this case your battery level.

Models are just factories: their internals can vary (it doesn't matter). The only constraint is that they accept configuration and return a (nested) object.

Note: This library actually doesn't prescribe any of the above, it's just recommended to get the best results in conjunction with wire().

Then, we'll wire that model's battery.getLevel() method up to a component. Normally, this would require defining a componentDidMount() method that calls a function (from ... somewhere?), waits for the returned Promise to resolve, then sets a value into state.

Using wire() though, we don't need lifecycle methods or state at all. We also don't need to invent a way to instance and access our model (often a singleton).

First, we'll build a model, hardware.js:

// A model is just a factory that returns an object with methods.
export default function hardwareModel() {
    return {
        battery: {
            // Methods return data, or a Promise resolving to data.
            getLevel() {
                return navigator.getBattery()
                    .then( battery => battery.level );
            }
        }
    };
}

Then, we write our simple "view" Component, battery-level.js:

import { h, Component } from 'preact';

export default class BatteryLevel extends Component {
    render({ batteryLevel='...' }) {
        // On initial render, we wont have received data from the Hardware model yet.
        // That will be indicated by the `batteryLevel` prop being undefined.
        // Thankfully, default parameter values take effect when a key is undefined!
        return <div>Battery Level: {batteryLevel}</div>
    }
}

Now we need to instance our model and expose it to all components using Provider. Provider just copies any props we give it into context.

Somewhere up the tree (often your root component or an app.js):

import { h, Component } from 'preact';
import Provider from 'preact-context-provider';
import hardwareModel from './hardware';
import BatteryLevel from './battery-level';

export default class App extends Component {
    hardware = hardwareModel();

    render() {
        return (
            <Provider hardware={this.hardware}>
                <BatteryLevel />
            </Provider>
        );
    }
}

Now we just have to wire that up to our view! Back in battery-level.js:

Note the first argument to wire() is the namespace of our model in context - defined by the prop name passed to <Provider>.

import { h, Component } from 'preact';
import wire from 'wiretie';

// Descendants of <Provider /> can subscribe to data from the model instance:
@wire('hardware', { batteryLevel: 'battery.getLevel' })
export default class BatteryLevel extends Component {
    render({ batteryLevel='...' }) {
        // On initial render, we wont have received data from the Hardware model yet.
        // That will be indicated by the `batteryLevel` prop being undefined.
        // Thankfully, default parameter values take effect when a key is undefined!
        return <div>Battery Level: {batteryLevel}</div>
    }
}

Finally, render the app!

import { h, render } from 'preact';
import App from './app';

render(<App />);

// Our app will first render this:
<span>Battery Level: ...</span>

// ...then automatically re-render once the Promise resolves:
<span>Battery Level: 1</span>

API

Table of Contents

wire

Creates a higher order component (HOC) that resolves (async) values from a model to props. This allows (but importantly abstracts) context access, and manages re-rendering in response to resolved data. wire() is simply a formalization of what is typically done as side-effects within componentDidMount().

Parameters

  • contextNamespace String? The context property at which to obtain a model instance. If empty, all of context is used.
  • mapToProps (Object | Function)? Maps incoming props to model method call descriptors: ['method.name', ...args]
  • mapModelToProps Function? Maps model properties/methods to props: model => ({ prop: model.property })

Examples

// resolves news.getTopStories(), passing it down as a "stories" prop
let withTopStories = wire('news', {
	stories: 'getTopStories'
});
export default withTopStories( props =>
	<ul>
		{ props.stories.map( item =>
			<li>{item.title}</li>
		) }
	</ul>
);
// resolves a news story by ID and passes it down as a "story" prop
let withStory = wire('news', props => ({
	story: ['getStory', props.id]
}));

// Simple "view" functional component to render a story
const StoryView = ({ story }) => (
	<div class="story">
		<h2>{story ? story.title : '...'}</h2>
		<p>{story && story.content}</p>
	</div>
);

// Wrap StoryView in the loader component created by wire()
const Story = withStory(StoryView);

//Get access to the wrapped Component
Story.getWrappedComponent() === StoryView; // true

// Provide a news model into context so Story can wire up to it
render(
	<Provider news={newsModel({ origin: '//news.api' })}>
		<div class="demo">
			<h1>News Story #1234:</h1>
			<Story id="1234" />
		</div>
	</Provider>
);

Returns Function wiring(Child) -> WireDataWrapper. The resulting HOC has a method getWrappedComponent() that returns the Child that was wrapped

props

Props passed to your wrapped component.

refresh

A refresh() method is passed down as a prop. Invoking this method re-fetches all data props, bypassing the cache.

rejected

If any Promises have been rejected, their values are available in a props.rejected Object. If there are no rejected promises, props.rejected is undefined.

Type: (Object<Error> | undefined)

pending

If any Promises are pending, the corresponding prop names will be keys in a props.pending Object. If there are no pending promises, props.pending is undefined.

Type: (Object<Boolean> | undefined)

License

FOSSA Status

More Repositories

1

preact-i18n

Simple localization for Preact.
JavaScript
205
star
2

dtk

DTK (data toolkit) is a suite of tools for parsing, analyzing, and graphing logs and other datasets.
Perl
112
star
3

preconf

A Higher Order Component that provides configuration as props.
JavaScript
89
star
4

gitprompt

An efficient, highly configurable prompt for Git status information in command-line shells
Perl
61
star
5

px

A Linux tool that provides progress and status information for command pipelines.
Perl
42
star
6

preact-context-provider

A generic <Provider /> for preact. It exposes any props you pass it into context.
JavaScript
33
star
7

sibyl

Sibyl is an online agile estimation tool that doesn't require sign-ups, entering user stories, or any other time consuming steps that keeps you from doing what matters: estimating on stories.
Go
32
star
8

eslint-config-synacor

Standard eslint rules for all Synacor javascript projects
JavaScript
22
star
9

python-cors

A Python package for dealing with HTTP requests and same-origin policies.
Python
14
star
10

jan

Jan is a simple library for making HTTP requests.
JavaScript
14
star
11

choose-files

Prompt the user to select files, then pass them to a callback.
JavaScript
14
star
12

frameworkless

A simple, flexible framework for developing medium-complexity web application front-ends.
JavaScript
12
star
13

maltypart

Maltypart is a simple multipart request builder
JavaScript
7
star
14

python-repath

A port of the node module path-to-regexp to Python.
Python
7
star
15

sync-git-utils

A collection of useful git utilities
Perl
6
star
16

ng-inject

Decorator that wraps a class in an Angular 1.x DI injector for the given dependencies.
JavaScript
5
star
17

eslint-plugin-preact-i18n

eslint plugin for users of the preact-i18n library
JavaScript
4
star
18

frameworkless-view

A simple view-presenter module for frameworkless
JavaScript
3
star
19

frameworkless-stage

Push views onto a stage, with optional view transitions.
JavaScript
2
star
20

argon2id

A utility library for serializing hashed passwords with its salt using argon2id
Go
2
star
21

test-mockpackages

Test::MockPackages is a package for mocking other packages as well as ensuring those packages are being used correctly.
Perl
1
star
22

nginx-module-txid120

Generate Transaction IDs in Nginx
C
1
star