0.1.2 • Published 2 years ago

react-viewcon v0.1.2

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

react-viewcon

Easily generate React functional components with view-controller separation.

Features

  • Easy separation of UI and logic by replacing React.FC with MVC function.
  • Controllers swappable at runtime - passed as a prop.
  • Multiple views with same controller or multiple controllers with the same view.
  • Built in, configurable generator that creates directories with all necessary files for you.
  • Easy way to keep your project structure unified.

Requirements

  • react@18.2.0 (should work on other versions, not tested),
  • typescript@4.7.0 or newer.

Recomended:

  • sass - for support of .scss files generated from CLI.

Installation

npm: npm i react-viewcon\ yarn: yarn add react-viewcon

Usage

Boilerplate

Minimal boilerplate consists of view and controller files.

View (my_new_component.tsx):

import * as React from 'react';
import MVC from 'react-viewcon';
import controller from './my_new_component.controller';

const MyNewComponent = MVC<{}, typeof controller>(controller,
    (p, c) => (
        <div>
            {p.children}
        </div>
    )
);

export default MyNewComponent;

Controller (my_new_component.controller.ts):

import {PropsWithChildren} from 'react';

export default (p: PropsWithChildren<{}>) => {
    return {

    };
};

Similar boilerplate, including scss file and separate props file can be generated using CLI.

Example

For simplicity this example demonstrates a simple button, that changes color upon clicking. The whole approach shows its true strength in complex components.

my_new_component.tsx:

import * as React from 'react';
import MVC from 'react-viewcon';

import {C, $C} from './my_new_component.controller';
import MyNewComponentProps from './my_new_component.props';
import './my_new_component.scss';

const MyNewComponent = MVC<MyNewComponentProps, $C>(C,
        (p, c) => (
                <div className='my-new-component'>
                  <button onClick={c.handleClick}
                          style={{backgroundColor: c.isOn ? 'green' : 'red'}}>
                    {p.buttonName}
                  </button>
                </div>
        )
);

export default MyNewComponent;

my_new_component.controller.ts:

import MyNewComponentProps from './my_new_component.props';
import {PropsWithChildren, useState} from 'react';

export const C = (p: PropsWithChildren<MyNewComponentProps>) => {

    const [isOn, setIsOn] = useState<boolean>(p.initialState);

    return {
        isOn,
        handleClick: () => setIsOn(!isOn)
    };
};

export type $C = typeof C;

my_new_component.props.ts:

export default interface MyNewComponentProps {
    buttonName: string;
    initialState: boolean;
}

Usage:

<MyNewComponent
    buttonName='my button'
    initialState={false}
/>

Now if we want to create another button - that for example always turn red after two seconds - all we have to do is swap controller. my_new_component.controller.timed.ts

import MyNewComponentProps from './my_new_component.props';
import {PropsWithChildren, useEffect, useState} from 'react';

const useTimedController = (p: PropsWithChildren<MyNewComponentProps>) => {

    const [isOn, setIsOn] = useState<boolean>(p.initialState);
    useEffect(() => {
       if (!isOn) return;
       const timeout = setTimeout(() => setIsOn(false), 2000);
       return () => clearTimeout(timeout);
    }, [isOn]);

    return {
        isOn,
        handleClick: () => setIsOn(!isOn)
    };
};

export default useTimedController;

Then all we need to make our button timed is to pass a controller prop:

<MyNewComponent
    buttonName='my button'
    initialState={false}
    controller={useTimedController}
/>

This can also work in other direction. For example we can write two views - for React and React Native, and then use only one controller for logic.

MVC function

The function takes two type variables:

  • P - props,
  • $C - type of controller that extends (p: PropsWithChildren<P>) => ReturnType<$C>.

And two parameters:

  • defaultController - default controller of type $C to use when controller prop is not passed,
  • view - the actual view function that takes p (props) and c (object returned by controller) and returns a ReactComponent.

The return value is a functional component, or to be exact: FC<P & ControlledProps<$C>>

To wrap up:

function MVC<P,$C>(
  defaultController: $C, 
  view: (p: PropsWithChildren<P>, c: ReturnType<$C>) => ReactElement<any, any>
): FC<P & ControlledProps<$C>>

Don't worry, you won't ever have to think about it.

CLI

react-viewcon uses generate-template-files package in order to generate viewcon boilerplate for you. To open generator prompt run: viewcon.

First select location using arrow keys and confirm using enter. Available locations can be configured.

? What do you want to generate? … 
Source root
Some other location

The prompt will ask you to insert component name - don't worry about the case, it will be handled automatically.

✔ What do you want to generate? · Some other location
? Insert component name › my new component 

Confirm or modify generated path:

? Output path: › ./src/example/my_new_component

Generated files:

 src
  ├── example
  │   └── my_new_component
  │       ├── my_new_component.controller.ts
  │       ├── my_new_component.props.ts
  │       ├── my_new_component.scss
  │       └── my_new_component.tsx

Configuration

Generator behavior can be modified using .viewconrc configuration file placed in project root directory. Configuration can be in json or ini format.

Configurable fields include:

  • directories - array of objects consisting of:
    • name - name displayed in the generator prompt,
    • path - actual path in which generator should create component.
  • filenameCase - case of component directory and filenames.
  • fileCase - case of .ts and .tsx files.

Available values of filenameCase and fileCase are:

  • 'noCase'
  • 'camelCase'
  • 'constantCase'
  • 'dotCase'
  • 'kebabCase'
  • 'lowerCase'
  • 'pascalCase'
  • 'pathCase'
  • 'sentenceCase'
  • 'snakeCase'
  • 'titleCase'

Example configuration (json format):

{
    "directories": [
        {
            "name": "Source root",
            "path": "./src"
        },
        {
            "name": "Some other location",
            "path": "./src/example"
        }
    ],
    "filenameCase": "camelCase",
    "fileCase": "snakeCase"
}

Default values:

{
    "directories": [{"name": "root", "path": "."}],
    "filenameCase": "snakeCase",
    "fileCase": "pascalCase"
}