mobx-strapi

MobX SDK for Strapi headless CMS


Keywords
mobx, observable, model, sdk, strapi, headless, cms, restful, api, state, store
License
LGPL-3.0
Install
npm install mobx-strapi@0.4.0-rc.2

Documentation

MobX RESTful

Common MobX abstract base Class & Decorator utilities for RESTful API.

Just define your Data models & Client HTTP methods, then leave rest of things to MobX!

MobX compatibility NPM Dependency CI & CD

NPM

Versions

SemVer status ES decorator MobX
>=0.7.0 ✅developing stage-3 >=6.11
<0.7.0 ❌deprecated stage-2 >=4 <6.11

Usage

package.json

{
    "dependencies": {
        "koajax": "^3.1.0",
        "mobx": "^6.13.5",
        "mobx-restful": "^2.1.0"
    }
}

tsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "moduleResolution": "Node",
        "useDefineForClassFields": true,
        "experimentalDecorators": false,
        "jsx": "react-jsx"
    }
}

Simple List

model/client.ts

import { HTTPClient } from 'koajax';

export const client = new HTTPClient({
    baseURI: 'https://api.github.com/',
    responseType: 'json'
});

model/Repository.ts

import { buildURLData } from 'web-utility';
import { Filter, ListModel } from 'mobx-restful';
import { components } from '@octokit/openapi-types';

import { client } from './client';

export type Organization = components['schemas']['organization-full'];
export type Repository = components['schemas']['minimal-repository'];

export class RepositoryModel<
    D extends Repository = Repository,
    F extends Filter<D> = Filter<D>
> extends ListModel<D, F> {
    client = client;
    baseURI = 'orgs/idea2app/repos';

    async loadPage(page: number, per_page: number) {
        const { body } = await this.client.get<D[]>(
            `${this.baseURI}?${buildURLData({ page, per_page })}`
        );
        const [_, organization] = this.baseURI.split('/');
        const {
            body: { public_repos }
        } = await this.client.get<Organization>(`orgs/${organization}`);

        return { pageData: body, totalCount: public_repos };
    }
}

export default new RepositoryModel();

page/Repository.tsx

Use WebCell as an Example

import { component, observer } from 'web-cell';

import repositoryStore from '../model/Repository';

@component({ tagName: 'repository-page' })
@observer
export class RepositoryPage extends HTMLElement {
    connectedCallback() {
        repositoryStore.getList();
    }

    disconnectedCallback() {
        repositoryStore.clear();
    }

    render() {
        const { currentPage } = repositoryStore;

        return (
            <ul>
                {currentPage.map(({ full_name, html_url }) => (
                    <li key={full_name}>
                        <a target="_blank" href={html_url}>
                            {full_name}
                        </a>
                    </li>
                ))}
            </ul>
        );
    }
}

Preload List

model/PreloadRepository.ts

import { buildURLData } from 'web-utility';
import { Buffer } from 'mobx-restful';

import { client } from './client';
import { Repository, RepositoryModel } from './Repository';

export class PreloadRepositoryModel extends Buffer<Repository>(
    RepositoryModel
) {
    client = client;
    baseURI = 'orgs/idea2app/repos';

    loadPage = RepositoryModel.prototype.loadPage;
}

export default new PreloadRepositoryModel();

Multiple Source List

model/MultipleRepository.ts

import { buildURLData, mergeStream } from 'web-utility';
import { Stream } from 'mobx-restful';
import { components } from '@octokit/openapi-types';

import { client } from './client';
import { Repository, RepositoryModel } from './Repository';

export type User = components['schemas']['public-user'];

export class MultipleRepository extends Stream<Repository>(RepositoryModel) {
    declare baseURI: string;
    client = client;

    async *getOrgRepos() {
        const {
            body: { public_repos }
        } = await this.client.get<Organization>('orgs/idea2app');

        this.totalCount = public_repos;

        for (let i = 1; ; i++) {
            const { body } = await this.client.get<Repository[]>(
                'orgs/idea2app/repos?page=' + i
            );
            if (!body[0]) break;

            yield* body;
        }
    }

    async *getUserRepos() {
        const {
            body: { public_repos }
        } = await this.client.get<User>('users/TechQuery');

        this.totalCount = public_repos;

        for (let i = 1; ; i++) {
            const { body } = await this.client.get<Repository[]>(
                'users/TechQuery/repos?page=' + i
            );
            if (!body[0]) break;

            yield* body;
        }
    }

    openStream() {
        return mergeStream(
            this.getOrgRepos.bind(this),
            this.getUserRepos.bind(this)
        );
    }
}

export default new MultipleRepository();

Data Persistence

@persist() & restore() functions give us a declarative way to save & restore data to/from IndexedBD, such as these following examples:

User Session

import { observable } from 'mobx';
import { BaseModel, persist, restore, toggle } from 'mobx-restful';
import { Day } from 'web-utility';

import { client } from './client';
import { User } from './User';

export class SessionModel extends BaseModel {
    baseURI = 'session';
    client = client;

    @persist({ expireIn: 15 * Day })
    @observable
    user?: User;

    restored = restore(this, 'Session');

    @toggle('uploading')
    async signIn(username: string, password: string) {
        const { body } = await this.client.post<User>('session', {
            username,
            password
        });
        return (this.user = body);
    }
}

export default new Session();

File Downloader

This module has been moved to MobX-downloader since MobX-RESTful v2.

List Cache

model/Party/Gift.ts
import { ListModel, persistList } from 'mobx-restful';

import { Gift } from '@my-scope/service-type';

import { client } from './client';

@persistList({
    storeKey: ({ partyId }) => `PartyGift-${partyId}`,
    expireIn: 2 * Day
})
export class PartyGiftModel extends ListModel<Gift> {
    baseURI = 'party';
    client = client;

    constructor(public partyId: number) {
        super();
        this.baseURI += `/${partyId}/gift`;
    }

    protected async loadPage(pageIndex: number, pageSize: number) {
        const { body } = await this.client.get<{
            count: number;
            list: Gift[];
        }>(`${this.baseURI}?${buildURLData({ pageIndex, pageSize })}`);

        return { pageData: body.list, totalCount: body.count };
    }
}
page/Party/Gift.tsx

This example page uses Cell Router to pass in partyId route parameter:

import { observable } from 'mobx';
import { component, observer, attribute } from 'web-cell';

import { PartyGiftModel } from '../../model/Party/Gift';

@component({ tagName: 'party-gift-page' })
@observer
export class PartyGiftPage extends HTMLElement {
    @attribute
    @observable
    accessor partyId = 0;

    @observable
    accessor store: PartyGiftModel | undefined;

    async connectedCallback() {
        this.store = new PartyGiftModel(this.partyId);

        await this.store.restored;
        // this calling will do nothing after the first loading
        // in list cache period
        await this.store.getAll();
    }

    render() {
        const { allItem } = this.store || {};

        return (
            <>
                <h1>Gift wall</h1>
                <ol>
                    {allItem.map(({ name, price }) => (
                        <li key={name}>
                            {name} - {price}
                        </li>
                    ))}
                </ol>
            </>
        );
    }
}

Wrapper

  1. Strapi v4
  2. GitHub
  3. Lark/FeiShu

Component

  1. Table, List & Form suite

Scaffold

  1. Client-side Rendering (React): https://github.com/idea2app/React-MobX-Bootstrap-ts
  2. Client-side Rendering (Vue): https://github.com/idea2app/Vue-MobX-Prime-ts
  3. Server-side Rendering (React): https://github.com/idea2app/Next-Bootstrap-ts
  4. Cross-end App (React): https://github.com/idea2app/Taro-Vant-MobX-ts

Limitation