├── database ├── .gitignore ├── mongo-init.js └── start.js ├── .prettierignore ├── .gitignore ├── .vscode └── extensions.json ├── frontend ├── src │ ├── assets │ │ └── layr-favicon-20201027.immutable.png │ ├── index.html │ ├── index.js │ └── components │ │ ├── common.js │ │ ├── frontend.js │ │ ├── movie-list.js │ │ └── movie.js ├── jsconfig.json ├── babel.config.js ├── simple-deployment.config.js ├── package.json └── webpack.config.js ├── backend ├── jsconfig.json ├── src │ ├── handler.js │ ├── server.js │ ├── http-server.js │ └── components │ │ └── movie.js ├── babel.config.js ├── simple-deployment.config.js └── package.json ├── .editorconfig ├── package.json └── README.md /database/.gitignore: -------------------------------------------------------------------------------- 1 | /data 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | dist 5 | build 6 | _private 7 | *.private.* 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "editorconfig.editorconfig"] 3 | } 4 | -------------------------------------------------------------------------------- /database/mongo-init.js: -------------------------------------------------------------------------------- 1 | db.createUser({ 2 | user: 'test', 3 | pwd: 'test', 4 | roles: [{role: 'readWrite', db: 'test'}] 5 | }); 6 | -------------------------------------------------------------------------------- /frontend/src/assets/layr-favicon-20201027.immutable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layrjs/crud-example-app-js-webpack/HEAD/frontend/src/assets/layr-favicon-20201027.immutable.png -------------------------------------------------------------------------------- /backend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "CommonJS", 5 | "checkJs": false, 6 | "experimentalDecorators": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "CommonJS", 5 | "checkJs": false, 6 | "experimentalDecorators": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/handler.js: -------------------------------------------------------------------------------- 1 | import {createAWSLambdaHandlerForComponentServer} from '@layr/aws-integration'; 2 | 3 | import {server} from './server'; 4 | 5 | export const handler = createAWSLambdaHandlerForComponentServer(server); 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /backend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | targets: {node: '10'}, 9 | loose: true 10 | } 11 | ] 12 | ]; 13 | 14 | const plugins = [ 15 | ['@babel/plugin-proposal-decorators', {legacy: true}], 16 | ['@babel/plugin-proposal-class-properties', {loose: true}] 17 | ]; 18 | 19 | return {presets, plugins}; 20 | }; 21 | -------------------------------------------------------------------------------- /backend/src/server.js: -------------------------------------------------------------------------------- 1 | import {ComponentServer} from '@layr/component-server'; 2 | import {MongoDBStore} from '@layr/mongodb-store'; 3 | 4 | import {Movie} from './components/movie'; 5 | 6 | const connectionString = process.env.MONGODB_STORE_CONNECTION_STRING; 7 | 8 | if (!connectionString) { 9 | throw new Error(`'MONGODB_STORE_CONNECTION_STRING' environment variable is missing`); 10 | } 11 | 12 | const store = new MongoDBStore(connectionString); 13 | store.registerRootComponent(Movie); 14 | 15 | export const server = new ComponentServer(Movie); 16 | -------------------------------------------------------------------------------- /backend/src/http-server.js: -------------------------------------------------------------------------------- 1 | import {ComponentHTTPServer} from '@layr/component-http-server'; 2 | 3 | import {server} from './server'; 4 | 5 | const backendURL = process.env.BACKEND_URL; 6 | 7 | if (!backendURL) { 8 | throw new Error(`'BACKEND_URL' environment variable is missing`); 9 | } 10 | 11 | const port = Number(new URL(backendURL).port); 12 | 13 | if (!port) { 14 | throw new Error(`'BACKEND_URL' environment variable should include a port`); 15 | } 16 | 17 | const httpServer = new ComponentHTTPServer(server, {port}); 18 | httpServer.start(); 19 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CRUD Example App 6 | 7 | 8 | <%= htmlWebpackPlugin.tags.headTags %> 9 | 10 | 11 | 12 |
13 | <%= htmlWebpackPlugin.tags.bodyTags %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | targets: {chrome: '55', safari: '11', firefox: '54'}, 9 | loose: true, 10 | modules: false 11 | } 12 | ], 13 | ['@babel/preset-react'] 14 | ]; 15 | 16 | const plugins = [ 17 | ['@babel/plugin-proposal-decorators', {legacy: true}], 18 | ['@babel/plugin-proposal-class-properties', {loose: true}] 19 | ]; 20 | 21 | return {presets, plugins}; 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/simple-deployment.config.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | const frontendURL = process.env.FRONTEND_URL; 3 | 4 | if (!frontendURL) { 5 | throw new Error(`'FRONTEND_URL' environment variable is missing`); 6 | } 7 | 8 | const domainName = new URL(frontendURL).hostname; 9 | 10 | return { 11 | type: 'website', 12 | provider: 'aws', 13 | domainName, 14 | files: ['./build'], 15 | customErrors: [{errorCode: 404, responseCode: 200, responsePage: 'index.html'}], 16 | aws: { 17 | region: 'us-west-2', 18 | cloudFront: { 19 | priceClass: 'PriceClass_100' 20 | } 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/components/movie.js: -------------------------------------------------------------------------------- 1 | import {Component, expose, validators} from '@layr/component'; 2 | import {Storable, primaryIdentifier, attribute} from '@layr/storable'; 3 | 4 | const {notEmpty} = validators; 5 | 6 | @expose({ 7 | find: {call: true}, 8 | prototype: { 9 | load: {call: true}, 10 | save: {call: true}, 11 | delete: {call: true} 12 | } 13 | }) 14 | export class Movie extends Storable(Component) { 15 | @expose({get: true, set: true}) @primaryIdentifier() id; 16 | 17 | @expose({get: true, set: true}) @attribute('string', {validators: [notEmpty()]}) title = ''; 18 | 19 | @expose({get: true, set: true}) @attribute('number?') year; 20 | 21 | @expose({get: true, set: true}) @attribute('string') country = ''; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import {getFrontend} from './components/frontend'; 5 | 6 | const backendURL = process.env.BACKEND_URL; 7 | 8 | if (!backendURL) { 9 | throw new Error(`'BACKEND_URL' environment variable is missing`); 10 | } 11 | 12 | (async () => { 13 | let content; 14 | 15 | try { 16 | const Frontend = await getFrontend({backendURL}); 17 | 18 | if (process.env.NODE_ENV !== 'production') { 19 | window.Frontend = Frontend; // For debugging 20 | } 21 | 22 | content = ; 23 | } catch (err) { 24 | console.error(err); 25 | 26 | content =
{err.stack}
; 27 | } 28 | 29 | ReactDOM.render(content, document.getElementById('root')); 30 | })(); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crud-example-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "Manuel Vila ", 6 | "license": "MIT", 7 | "scripts": { 8 | "deploy": "(cd ./backend && npm run deploy) && (cd ./frontend && npm run deploy)", 9 | "postinstall": "(cd ./frontend && npm install) && (cd ./backend && npm install)", 10 | "start": "concurrently --names=frontend,backend,database --prefix-colors=green,blue,gray --kill-others \"(cd ./frontend && npm run start)\" \"(cd ./backend && npm run start)\" \"(cd ./database && node ./start.js)\"", 11 | "update": "(cd ./frontend && npm update) && (cd ./backend && npm update)" 12 | }, 13 | "prettier": "@mvila/prettierrc", 14 | "devDependencies": { 15 | "@mvila/prettierrc": "^1.1.0", 16 | "concurrently": "^5.3.0", 17 | "prettier": "^2.1.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /database/start.js: -------------------------------------------------------------------------------- 1 | const {execFileSync} = require('child_process'); 2 | 3 | const connectionString = process.env.MONGODB_STORE_CONNECTION_STRING; 4 | 5 | if (!connectionString) { 6 | throw new Error(`'MONGODB_STORE_CONNECTION_STRING' environment variable is missing`); 7 | } 8 | 9 | const port = Number(new URL(connectionString).port || '27017'); 10 | 11 | process.on('SIGINT', () => {}); 12 | 13 | execFileSync( 14 | 'docker', 15 | [ 16 | 'run', 17 | '--name', 18 | 'crud-example-app-database', 19 | '--rm', 20 | '--volume', 21 | `${__dirname}/data:/data/db`, 22 | '--volume', 23 | `${__dirname}/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro`, 24 | '--publish', 25 | `127.0.0.1:${port}:27017`, 26 | '--env', 27 | 'MONGO_INITDB_ROOT_USERNAME=test', 28 | '--env', 29 | 'MONGO_INITDB_ROOT_PASSWORD=test', 30 | 'mongo:4' 31 | ], 32 | {cwd: __dirname, stdio: 'inherit'} 33 | ); 34 | -------------------------------------------------------------------------------- /backend/simple-deployment.config.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | const backendURL = process.env.BACKEND_URL; 3 | 4 | if (!backendURL) { 5 | throw new Error(`'BACKEND_URL' environment variable is missing`); 6 | } 7 | 8 | const domainName = new URL(backendURL).hostname; 9 | 10 | const connectionString = process.env.MONGODB_STORE_CONNECTION_STRING; 11 | 12 | if (!connectionString) { 13 | throw new Error(`'MONGODB_STORE_CONNECTION_STRING' environment variable is missing`); 14 | } 15 | 16 | return { 17 | type: 'function', 18 | provider: 'aws', 19 | domainName, 20 | files: ['./build'], 21 | main: './build/handler.js', 22 | includeDependencies: true, 23 | environment: { 24 | MONGODB_STORE_CONNECTION_STRING: connectionString 25 | }, 26 | aws: { 27 | region: 'us-west-2', 28 | lambda: { 29 | memorySize: 1024, 30 | timeout: 15 31 | } 32 | } 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /frontend/src/components/common.js: -------------------------------------------------------------------------------- 1 | import {Component} from '@layr/component'; 2 | import React from 'react'; 3 | import {view, useDelay} from '@layr/react-integration'; 4 | 5 | export class Common extends Component { 6 | @view() static LoadingMessage() { 7 | return ( 8 | 9 |
Loading...
10 |
11 | ); 12 | } 13 | 14 | @view() static ErrorMessage({message = 'Sorry, something went wrong.', onRetry}) { 15 | return ( 16 |
17 |

{message}

18 |

19 | 20 |

21 |
22 | ); 23 | } 24 | 25 | @view() static RouteNotFound() { 26 | return
Sorry, there is nothing here.
; 27 | } 28 | 29 | @view() static Delayed({duration = 200, children}) { 30 | const [isElapsed] = useDelay(duration); 31 | 32 | if (isElapsed) { 33 | return children; 34 | } 35 | 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crud-example-app-backend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "Manuel Vila ", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf ./build && babel ./src --out-dir ./build", 9 | "deploy": "npm run build && simple-deployment", 10 | "start": "nodemon --watch ./src --exec babel-node ./src/http-server.js" 11 | }, 12 | "dependencies": { 13 | "@layr/aws-integration": "^1.0.20", 14 | "@layr/component": "^1.0.20", 15 | "@layr/component-server": "^1.0.18", 16 | "@layr/mongodb-store": "^1.1.14", 17 | "@layr/storable": "^1.1.4" 18 | }, 19 | "devDependencies": { 20 | "@babel/cli": "^7.11.5", 21 | "@babel/core": "^7.11.5", 22 | "@babel/node": "^7.10.5", 23 | "@babel/plugin-proposal-class-properties": "^7.10.4", 24 | "@babel/plugin-proposal-decorators": "^7.12.12", 25 | "@babel/preset-env": "^7.11.5", 26 | "@layr/component-http-server": "^1.0.18", 27 | "nodemon": "^2.0.4", 28 | "simple-deployment": "^0.1.46" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CRUD Example App 2 | 3 | > **This Layr example app is deprecated.** 4 | 5 | A simple example showing how to build a full-stack CRUD app with Layr. 6 | 7 | ## Install 8 | 9 | Install the npm dependencies with: 10 | 11 | ```sh 12 | npm install 13 | ``` 14 | 15 | Make sure you have [Docker](https://www.docker.com/) installed as it is used to run the database (MongoDB) when running the app in development mode. 16 | 17 | ## Usage 18 | 19 | ### Running the app in development mode 20 | 21 | Execute the following command: 22 | 23 | ```sh 24 | FRONTEND_URL=http://localhost:16577 \ 25 | BACKEND_URL=http://localhost:16578 \ 26 | MONGODB_STORE_CONNECTION_STRING=mongodb://test:test@localhost:16579/test \ 27 | npm run start 28 | ``` 29 | 30 | The app should then be available at http://localhost:16577. 31 | 32 | ### Debugging 33 | 34 | #### Client 35 | 36 | Add the following entry in the local storage of your browser: 37 | 38 | ``` 39 | | Key | Value | 40 | | ----- | --------- | 41 | | debug | layr:* | 42 | ``` 43 | 44 | #### Server 45 | 46 | Add the following environment variables when starting the app: 47 | 48 | ```sh 49 | DEBUG=layr:* DEBUG_DEPTH=10 50 | ``` 51 | -------------------------------------------------------------------------------- /frontend/src/components/frontend.js: -------------------------------------------------------------------------------- 1 | import {Component, provide} from '@layr/component'; 2 | import {Storable} from '@layr/storable'; 3 | import {ComponentHTTPClient} from '@layr/component-http-client'; 4 | import React from 'react'; 5 | import {view, useBrowserRouter} from '@layr/react-integration'; 6 | 7 | import {MovieList} from './movie-list'; 8 | import {Movie} from './movie'; 9 | import {Common} from './common'; 10 | 11 | export const getFrontend = async ({backendURL}) => { 12 | const client = new ComponentHTTPClient(backendURL, {mixins: [Storable]}); 13 | 14 | const BackendMovie = await client.getComponent(); 15 | 16 | class Frontend extends Component { 17 | @provide() static MovieList = MovieList; 18 | @provide() static Movie = Movie(BackendMovie); 19 | @provide() static Common = Common; 20 | 21 | @view() static Main() { 22 | const [router, isReady] = useBrowserRouter(this); 23 | 24 | if (!isReady) { 25 | return null; 26 | } 27 | 28 | const content = router.callCurrentRoute({fallback: this.Common.RouteNotFound}); 29 | 30 | return ( 31 |
32 |

CRUD example app

33 | {content} 34 |
35 | ); 36 | } 37 | } 38 | 39 | return Frontend; 40 | }; 41 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crud-example-app-frontend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "Manuel Vila ", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "webpack --mode=production", 9 | "deploy": "npm run build && simple-deployment", 10 | "start": "webpack serve --mode=development" 11 | }, 12 | "dependencies": { 13 | "@layr/component": "^1.0.20", 14 | "@layr/component-http-client": "^1.0.21", 15 | "@layr/react-integration": "^1.0.20", 16 | "@layr/routable": "^1.0.19", 17 | "@layr/storable": "^1.1.4", 18 | "react": "^16.13.1", 19 | "react-dom": "^16.13.1" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.12.10", 23 | "@babel/plugin-proposal-class-properties": "^7.10.4", 24 | "@babel/plugin-proposal-decorators": "^7.12.12", 25 | "@babel/preset-env": "^7.12.11", 26 | "@babel/preset-react": "^7.12.10", 27 | "babel-loader": "^8.2.2", 28 | "clean-webpack-plugin": "^3.0.0", 29 | "html-loader": "^1.3.0", 30 | "html-webpack-plugin": "^4.4.1", 31 | "simple-deployment": "^0.1.46", 32 | "terser-webpack-plugin": "^3.1.0", 33 | "webpack": "^5.11.1", 34 | "webpack-cli": "^4.3.1", 35 | "webpack-dev-server": "^3.11.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/components/movie-list.js: -------------------------------------------------------------------------------- 1 | import {Component, attribute, consume} from '@layr/component'; 2 | import {Routable, route} from '@layr/routable'; 3 | import React from 'react'; 4 | import {view, useAsyncMemo} from '@layr/react-integration'; 5 | 6 | export class MovieList extends Routable(Component) { 7 | @consume() static Movie; 8 | @consume() static Common; 9 | 10 | @attribute('Movie[]?') items; 11 | 12 | @view() static Layout({children}) { 13 | return ( 14 |
15 |

Movies

16 | {children} 17 |
18 | ); 19 | } 20 | 21 | @route('/movies', {aliases: ['/']}) @view() static Main() { 22 | const [movieList, isLoading, loadingError, retryLoading] = useAsyncMemo(async () => { 23 | const movieList = new this(); 24 | 25 | movieList.items = await this.Movie.find( 26 | {}, 27 | {title: true, year: true}, 28 | {sort: {year: 'desc', title: 'asc'}} 29 | ); 30 | 31 | return movieList; 32 | }, []); 33 | 34 | if (isLoading) { 35 | return ; 36 | } 37 | 38 | if (loadingError) { 39 | return ( 40 | 44 | ); 45 | } 46 | 47 | return ( 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | @view() Main() { 55 | const {Movie} = this.constructor; 56 | 57 | return ( 58 | <> 59 |
    60 | {this.items.map((movie) => ( 61 | 62 | ))} 63 |
64 |

65 | 66 |

67 | 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 3 | const {CleanWebpackPlugin} = require('clean-webpack-plugin'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | const path = require('path'); 6 | 7 | module.exports = (env, argv) => { 8 | const isProduction = argv.mode === 'production'; 9 | 10 | let port; 11 | 12 | if (!isProduction) { 13 | const frontendURL = process.env.FRONTEND_URL; 14 | 15 | if (!frontendURL) { 16 | throw new Error(`'FRONTEND_URL' environment variable is missing`); 17 | } 18 | 19 | port = Number(new URL(frontendURL).port); 20 | 21 | if (!port) { 22 | throw new Error(`'FRONTEND_PORT' environment variable should include a port`); 23 | } 24 | } 25 | 26 | return { 27 | entry: './src/index.js', 28 | output: { 29 | path: path.join(__dirname, 'build'), 30 | filename: isProduction ? '[name].[contenthash].immutable.js' : 'bundle.js', 31 | publicPath: '/' 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.js$/, 37 | include: path.join(__dirname, 'src'), 38 | loader: 'babel-loader' 39 | } 40 | ] 41 | }, 42 | resolve: { 43 | alias: { 44 | 'react': path.resolve('./node_modules/react'), 45 | 'react-dom': path.resolve('./node_modules/react-dom') 46 | } 47 | }, 48 | plugins: [ 49 | new CleanWebpackPlugin(), 50 | new HtmlWebPackPlugin({ 51 | template: './src/index.html', 52 | favicon: './src/assets/layr-favicon-20201027.immutable.png', 53 | inject: false 54 | }), 55 | new webpack.EnvironmentPlugin(['BACKEND_URL']) 56 | ], 57 | ...(isProduction 58 | ? { 59 | optimization: { 60 | minimizer: [new TerserPlugin({terserOptions: {keep_classnames: true}})] 61 | } 62 | } 63 | : { 64 | devtool: 'eval-cheap-module-source-map', 65 | devServer: { 66 | contentBase: './build/dev', 67 | port, 68 | historyApiFallback: true 69 | } 70 | }) 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /frontend/src/components/movie.js: -------------------------------------------------------------------------------- 1 | import {consume} from '@layr/component'; 2 | import {Routable, route} from '@layr/routable'; 3 | import React, {useMemo} from 'react'; 4 | import {view, useAsyncMemo, useAsyncCallback} from '@layr/react-integration'; 5 | 6 | export function Movie(Base) { 7 | class Movie extends Routable(Base) { 8 | @consume() static MovieList; 9 | @consume() static Common; 10 | 11 | @view() static Layout({children}) { 12 | return ( 13 |
14 |

Movie

15 | {children} 16 |
17 | ); 18 | } 19 | 20 | @view() static Loader({id, children}) { 21 | const [movie, isLoading, loadingError, retryLoading] = useAsyncMemo(async () => { 22 | return await this.get(id, {title: true, year: true, country: true}); 23 | }, [id]); 24 | 25 | if (isLoading) { 26 | return ; 27 | } 28 | 29 | if (loadingError) { 30 | return ( 31 | 35 | ); 36 | } 37 | 38 | return children(movie); 39 | } 40 | 41 | @route('/movies/:id') @view() static Main({id}) { 42 | return ( 43 | 44 | {(movie) => } 45 |

46 | ‹ Back 47 |

48 |
49 | ); 50 | } 51 | 52 | @view() Main() { 53 | const {MovieList, Editor} = this.constructor; 54 | 55 | const [handleDelete, isDeleting, deletingError] = useAsyncCallback(async () => { 56 | await this.delete(); 57 | 58 | MovieList.Main.navigate(); 59 | }, []); 60 | 61 | return ( 62 |
63 | {deletingError &&

Sorry, something went wrong while deleting the movie.

} 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
Title:{this.title}
Year:{this.year}
Country:{this.country}
80 |

81 | 84 |   85 | 88 |

89 |
90 | ); 91 | } 92 | 93 | @route('/movies/-/create') @view() static Creator() { 94 | const movie = useMemo(() => { 95 | return new this(); 96 | }, []); 97 | 98 | return ( 99 | 100 | 101 | 102 | ); 103 | } 104 | 105 | @view() Creator() { 106 | const {MovieList} = this.constructor; 107 | 108 | const [handleSave, , savingError] = useAsyncCallback(async () => { 109 | await this.save(); 110 | 111 | MovieList.Main.navigate(); 112 | }, []); 113 | 114 | return ( 115 |
116 | {savingError &&

Sorry, something went wrong while saving the movie.

} 117 | 118 |

119 | ‹ Back 120 |

121 |
122 | ); 123 | } 124 | 125 | @route('/movies/:id/edit') @view() static Editor({id}) { 126 | return ( 127 | 128 | {(movie) => } 129 | 130 | ); 131 | } 132 | 133 | @view() Editor() { 134 | const {Main} = this.constructor; 135 | 136 | const forkedMovie = useMemo(() => this.fork(), []); 137 | 138 | const [handleSave, , savingError] = useAsyncCallback(async () => { 139 | await forkedMovie.save(); 140 | 141 | this.merge(forkedMovie); 142 | 143 | Main.navigate(this); 144 | }, [forkedMovie]); 145 | 146 | return ( 147 |
148 | {savingError &&

Sorry, something went wrong while saving the movie.

} 149 | 150 |

151 | ‹ Back 152 |

153 |
154 | ); 155 | } 156 | 157 | @view() Form({onSubmit}) { 158 | const [handleSubmit, isSubmitting] = useAsyncCallback( 159 | async (event) => { 160 | event.preventDefault(); 161 | await onSubmit(); 162 | }, 163 | [onSubmit] 164 | ); 165 | 166 | return ( 167 |
168 | 169 | 170 | 171 | 172 | 181 | 182 | 183 | 184 | 192 | 193 | 194 | 195 | 203 | 204 | 205 |
Title: 173 | { 176 | this.title = event.target.value; 177 | }} 178 | required 179 | /> 180 |
Year: 185 | { 188 | this.year = Number(event.target.value) || undefined; 189 | }} 190 | /> 191 |
Country: 196 | { 199 | this.country = event.target.value; 200 | }} 201 | /> 202 |
206 |

207 | 210 |

211 |
212 | ); 213 | } 214 | 215 | @view() ListItem() { 216 | const {Main} = this.constructor; 217 | 218 | return ( 219 |
  • 220 | {this.title} 221 | {this.year !== undefined ? ` (${this.year})` : ''} 222 |
  • 223 | ); 224 | } 225 | } 226 | 227 | return Movie; 228 | } 229 | --------------------------------------------------------------------------------