├── .gitignore
├── App
├── Bridge
│ └── ViewiReactBridge.php
├── Controller
│ ├── AuthSessionAction.php
│ ├── AuthTokenAction.php
│ ├── PostsAction.php
│ └── PostsActionAsync.php
├── Message
│ └── RawJsonResponse.php
└── Middleware
│ ├── RequestsHandlerMiddleware.php
│ └── StaticFilesMiddleware.php
├── DemoOverview.md
├── LICENSE
├── README.md
├── composer.json
├── composer.lock
├── public
├── about.txt
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── main.css
└── site.webmanifest
├── screenshots
├── async.png
├── asyncInterceptors.png
├── asyncMiddleware.png
├── counter.png
├── homePage.png
└── todo.png
├── server.php
└── viewi-app
├── Components
├── Models
│ └── PostModel.php
├── Services
│ ├── Interceptors
│ │ ├── AuthorizationInterceptor.php
│ │ └── SessionInterceptor.php
│ ├── Middleware
│ │ ├── AuthGuard.php
│ │ ├── AuthGuardFail.php
│ │ └── SessionGuard.php
│ └── Reducers
│ │ ├── CounterReducer.php
│ │ └── TodoReducer.php
└── Views
│ ├── AsyncTest
│ ├── AsyncTestComponent.html
│ ├── AsyncTestComponent.php
│ ├── PostComponent.html
│ └── PostComponent.php
│ ├── Common
│ ├── MenuBar.html
│ └── MenuBar.php
│ ├── Counter
│ ├── Counter.html
│ └── Counter.php
│ ├── Home
│ ├── HomePage.html
│ └── HomePage.php
│ ├── InterceptorsTest
│ ├── InterceptorsTestComponent.html
│ └── InterceptorsTestComponent.php
│ ├── Layouts
│ ├── Layout.html
│ └── Layout.php
│ ├── MiddlewareTest
│ ├── MiddlewareFailTestComponent.html
│ ├── MiddlewareFailTestComponent.php
│ ├── MiddlewareTestComponent.html
│ └── MiddlewareTestComponent.php
│ ├── NotFound
│ ├── NotFoundPage.html
│ └── NotFoundPage.php
│ ├── Pages
│ ├── CounterPage.html
│ ├── CounterPage.php
│ ├── CurrentUrlTestPage.html
│ ├── CurrentUrlTestPage.php
│ ├── RedirectTestComponent.html
│ ├── RedirectTestComponent.php
│ ├── TodoAppPage.html
│ └── TodoAppPage.php
│ ├── StatefulCounter
│ ├── StatefulCounter.html
│ └── StatefulCounter.php
│ ├── StatefulTodoApp
│ ├── StatefulTodoApp.html
│ └── StatefulTodoApp.php
│ └── TodoApp
│ ├── TodoApp.html
│ ├── TodoApp.php
│ ├── TodoList.html
│ └── TodoList.php
├── assets
├── app.css
└── mui.css
├── build.php
├── config.php
├── js
├── build.mjs
├── modules
│ └── main
│ │ └── index.ts
├── package.json
└── viewi
│ ├── core
│ ├── anchor
│ │ ├── anchor.ts
│ │ ├── anchors.ts
│ │ ├── createAnchorNode.ts
│ │ ├── foreachAnchorEnum.ts
│ │ ├── getAnchor.ts
│ │ ├── nodeAnchor.ts
│ │ └── textAnchor.ts
│ ├── component
│ │ ├── baseComponent.ts
│ │ ├── componentMetaData.ts
│ │ ├── componentsJson.ts
│ │ ├── componentsMeta.ts
│ │ ├── iStartUp.ts
│ │ ├── isComponent.ts
│ │ ├── makeGlobal.ts
│ │ ├── makeGlobalMethod.ts
│ │ └── reserverProps.ts
│ ├── di
│ │ ├── delay.ts
│ │ ├── diContainer.ts
│ │ ├── factory.ts
│ │ ├── globalScope.ts
│ │ ├── register.ts
│ │ ├── resolve.ts
│ │ ├── scopeType.ts
│ │ └── setUp.ts
│ ├── directive
│ │ ├── conditionalDirective.ts
│ │ ├── directive.ts
│ │ ├── directiveMap.ts
│ │ ├── directiveStorageType.ts
│ │ └── directiveType.ts
│ ├── environment
│ │ └── platform.ts
│ ├── events
│ │ └── resolver.ts
│ ├── helpers
│ │ ├── ensureType.ts
│ │ ├── isBlob.ts
│ │ ├── isSvg.ts
│ │ └── svgNameSpace.ts
│ ├── http
│ │ ├── httpClient.ts
│ │ ├── iHttpInterceptor.ts
│ │ ├── iRequestHandler.ts
│ │ ├── iResponseHandler.ts
│ │ ├── injectScript.ts
│ │ ├── methodType.ts
│ │ ├── request.ts
│ │ ├── response.ts
│ │ └── runRequest.ts
│ ├── hydrate
│ │ ├── hydrateComment.ts
│ │ ├── hydrateRaw.ts
│ │ ├── hydrateTag.ts
│ │ └── hydrateText.ts
│ ├── lifecycle
│ │ ├── arrayScope.ts
│ │ ├── contextScope.ts
│ │ ├── dispose.ts
│ │ ├── iDestroyable.ts
│ │ ├── imiddleware.ts
│ │ ├── propsContext.ts
│ │ └── scopeState.ts
│ ├── node
│ │ ├── htmlNodeType.ts
│ │ ├── inputType.ts
│ │ ├── nodeType.ts
│ │ ├── slots.ts
│ │ ├── templateNode.ts
│ │ └── unpack.ts
│ ├── portal
│ │ ├── portal.ts
│ │ ├── portals.ts
│ │ └── renderPortal.ts
│ ├── reactivity
│ │ ├── handlers
│ │ │ ├── getComponentModelHandler.ts
│ │ │ ├── getModelHandler.ts
│ │ │ ├── modelHandler.ts
│ │ │ ├── updateComment.ts
│ │ │ ├── updateComponentModel.ts
│ │ │ ├── updateModelValue.ts
│ │ │ └── updateProp.ts
│ │ ├── makeProxy.ts
│ │ ├── makeProxyOrigin.ts
│ │ └── track.ts
│ ├── render
│ │ ├── iRenderable.ts
│ │ ├── render.ts
│ │ ├── renderApp.ts
│ │ ├── renderAttributeValue.ts
│ │ ├── renderComponent.ts
│ │ ├── renderDynamic.ts
│ │ ├── renderForeach.ts
│ │ ├── renderIf.ts
│ │ ├── renderRaw.ts
│ │ └── renderText.ts
│ ├── router
│ │ ├── handleUrl.ts
│ │ ├── locationScope.ts
│ │ ├── routeItem.ts
│ │ ├── routeRecord.ts
│ │ ├── router.ts
│ │ └── watchLinks.ts
│ └── viewi.ts
│ └── index.ts
├── routes.php
└── viewi.php
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.phar
2 | /vendor/
3 | **/build/**/*
4 | !**/build/.gitkeep
5 | **/build/**/*
6 | !**/build/.gitkeep
7 | **/viewi-react/**/*
8 | viewi-app/publicConfig.php
9 | viewi-app/js/app/**/*
10 | viewi-app/js/dist/**/*
11 | **/node_modules
12 |
--------------------------------------------------------------------------------
/App/Bridge/ViewiReactBridge.php:
--------------------------------------------------------------------------------
1 | requestHandler = $requestHandler;
25 | }
26 |
27 | public function request(Request $request, Engine $engine): mixed
28 | {
29 | if ($request->isExternal) {
30 | // HTTP call to some third party resource
31 | $browser = new \React\Http\Browser();
32 | $promise = $browser->request($request->method, $request->url, $request->headers, $request->body ? json_encode($request->body) : '');
33 | $response = await($promise);
34 | return @json_decode($response->getBody(), true);
35 | }
36 |
37 | $reactRequest = new ServerRequest($request->method, $request->url, $request->headers, $request->body ? json_encode($request->body) : '');
38 | $response = ($this->requestHandler)($reactRequest, true);
39 | if ($response instanceof PromiseInterface) {
40 | $response = await($response);
41 | }
42 | $data = null;
43 | if ($response instanceof RawJsonResponse) {
44 | $data = $response->getData();
45 | $response = $response->getResponse();
46 | } else {
47 | $data = @json_decode($response->getBody()->__toString(), true);
48 | }
49 | /**
50 | * @var \React\Http\Message\Response $response
51 | */
52 | $viewiResponse = new \Viewi\Components\Http\Message\Response($request->url, $response->getStatusCode(), $response->getReasonPhrase(), $response->getHeaders(), $data);
53 | return $viewiResponse;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/App/Controller/AuthSessionAction.php:
--------------------------------------------------------------------------------
1 | '000-1111-2222']);
18 | $resolve($response);
19 | });
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/App/Controller/AuthTokenAction.php:
--------------------------------------------------------------------------------
1 | getAttribute('params')['valid'] === 'true';
19 | if (!$valid) {
20 | $resolve(new Response(401, [], ''));
21 | return;
22 | }
23 | $response = new RawJsonResponse(['token' => 'base64string']);
24 | $resolve($response);
25 | });
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/App/Controller/PostsAction.php:
--------------------------------------------------------------------------------
1 | Id = $request->getAttribute('params')['id'] ?? 0;
15 | $post->Name = 'Viewi ft. ReactPHP';
16 | $post->Version = 1;
17 | $response = new RawJsonResponse($post);
18 | return $response;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/App/Controller/PostsActionAsync.php:
--------------------------------------------------------------------------------
1 | getAttribute('params')['ms'] ?? 50;
17 | if ($ms > 5000) // we don't want it to be more than 5 sec
18 | {
19 | $ms = 5000;
20 | }
21 | // Simulating I/O delay (DB/file read) with timer
22 | Loop::addTimer($ms / 1000 + (rand(1, 20) / 100), function () use ($resolve, $request) {
23 | $postId = $request->getAttribute('params')['id'] ?? 0;
24 | $post = new PostModel();
25 | $post->Id = $postId;
26 | $post->Name = "Viewi ft. ReactPHP $postId";
27 | $post->Version = $postId + 1000;
28 | $response = new RawJsonResponse($post);
29 | // echo "request Loop:1. \n";
30 | $resolve($response);
31 | });
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/App/Message/RawJsonResponse.php:
--------------------------------------------------------------------------------
1 | data = $data;
17 | $this->response = (Response::json($data))->withStatus($status);
18 | }
19 |
20 | public function getStatusCode()
21 | {
22 | return $this->response->getStatusCode();
23 | }
24 |
25 | public function withStatus(int $code, string $reasonPhrase = '')
26 | {
27 | $this->response = $this->response->withStatus($code, $reasonPhrase);
28 | return $this;
29 | }
30 |
31 | public function getReasonPhrase()
32 | {
33 | return $this->response->getReasonPhrase();
34 | }
35 |
36 | public function getProtocolVersion()
37 | {
38 | return $this->response->getProtocolVersion();
39 | }
40 |
41 | public function withProtocolVersion(string $version)
42 | {
43 | $this->response = $this->response->withProtocolVersion($version);
44 | return $this;
45 | }
46 |
47 | public function getHeaders()
48 | {
49 | return $this->response->getHeaders();
50 | }
51 |
52 | public function hasHeader(string $name)
53 | {
54 | return $this->response->hasHeader($name);
55 | }
56 |
57 | public function getHeader(string $name)
58 | {
59 | return $this->response->getHeader($name);
60 | }
61 |
62 | public function getHeaderLine(string $name)
63 | {
64 | return $this->response->getHeaderLine($name);
65 | }
66 |
67 | public function withHeader(string $name, $value)
68 | {
69 | $this->response = $this->response->withHeader($name, $value);
70 | return $this;
71 | }
72 |
73 | public function withAddedHeader(string $name, $value)
74 | {
75 | $this->response = $this->response->withAddedHeader($name, $value);
76 | return $this;
77 | }
78 |
79 | public function withoutHeader(string $name)
80 | {
81 | $this->response = $this->response->withoutHeader($name);
82 | return $this;
83 | }
84 |
85 | public function getBody()
86 | {
87 | return $this->response->getBody();
88 | }
89 |
90 | public function withBody(StreamInterface $body)
91 | {
92 | $this->response = $this->response->withBody($body);
93 | return $this;
94 | }
95 |
96 | public function getData(): mixed
97 | {
98 | return $this->data;
99 | }
100 |
101 | public function getResponse(): Response
102 | {
103 | return $this->response;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/App/Middleware/RequestsHandlerMiddleware.php:
--------------------------------------------------------------------------------
1 | router->resolve($request->getUri()->getPath(), $request->getMethod());
28 | if ($match === null) {
29 | throw new Exception('No route was matched!');
30 | }
31 | /** @var RouteItem */
32 | $routeItem = $match['item'];
33 | $action = $routeItem->action;
34 | $response = '';
35 | if (is_callable($action) && !is_string($action)) {
36 | if ($match['params']) {
37 | $request = $request->withAttribute('params', $match['params']);
38 | }
39 | $response = $action($request);
40 | } elseif ($action instanceof ComponentRoute) {
41 | $viewiRequest = new Request($request->getUri()->getPath(), strtolower($request->getMethod()));
42 | $response = $this->viewiApp->engine()->render($action->component, $match['params'], $viewiRequest);
43 | } else {
44 | throw new Exception('Unknown action type.');
45 | }
46 |
47 | // if ($response instanceof PromiseInterface) {
48 | // return $response;
49 | // // $response = await($response);
50 | // }
51 | if (is_string($response)) { // string as html
52 | return new Response(
53 | 200,
54 | array(
55 | 'Content-Type' => 'text/html; charset=utf-8'
56 | ),
57 | $response
58 | );
59 | } elseif ($response instanceof ViewiResponse) {
60 | return new Response(
61 | isset($response->headers['Location']) ? 302 : $response->status,
62 | $response->headers,
63 | is_string($response->body) ? $response->body : json_encode($response->body)
64 | );
65 | } elseif ($response instanceof RawJsonResponse) {
66 | return $keepRaw ? $response : $response->getResponse();
67 | } elseif ($response instanceof PromiseInterface) {
68 | return $response;
69 | // $response = await($response);
70 | } else { // json
71 | new Response(
72 | 200,
73 | array(
74 | 'Content-Type' => 'application/json'
75 | ),
76 | json_encode($response)
77 | );
78 | }
79 | } catch (Throwable $t) {
80 | echo $t->getMessage() . "\n";
81 | echo $t->getTraceAsString() . "\n";
82 | // !! For production: Consider using $reject and don't output stack trace
83 | return new Response(
84 | 500,
85 | array(
86 | 'Content-Type' => 'text/text'
87 | ),
88 | $t->getMessage() . $t->getTraceAsString()
89 | );
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/App/Middleware/StaticFilesMiddleware.php:
--------------------------------------------------------------------------------
1 | directory = $directory;
14 | }
15 |
16 | public function __invoke(\Psr\Http\Message\ServerRequestInterface $request, callable $next)
17 | {
18 | $filePath = $request->getUri()->getPath();
19 | $file = $this->directory . $filePath;
20 | if (file_exists($file) && !is_dir($file)) {
21 | $fileExt = pathinfo($file, PATHINFO_EXTENSION);
22 | $contentType = 'text/text';
23 | switch ($fileExt) {
24 | case 'js': {
25 | $contentType = 'application/javascript';
26 | break;
27 | }
28 | case 'json': {
29 | $contentType = 'application/json';
30 | break;
31 | }
32 | case 'css': {
33 | $contentType = 'text/css';
34 | break;
35 | }
36 | case 'ico': {
37 | $contentType = 'image/x-icon';
38 | break;
39 | }
40 | }
41 | return new Response(200, ['Content-Type' => $contentType], file_get_contents($file));
42 | }
43 | return $next($request);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/DemoOverview.md:
--------------------------------------------------------------------------------
1 | # ReactPHP and Viewi demo application overview
2 |
3 | ## Home page
4 |
5 | [http://localhost:8080/](http://localhost:8080/)
6 |
7 | Home page loads one post using HttpClient to call an API
8 |
9 | 
10 |
11 | ```php
12 | http->get('/api/posts/45')->then(
32 | function (PostModel $post) {
33 | $this->post = $post;
34 | },
35 | function ($error) {
36 | echo $error;
37 | }
38 | );
39 | }
40 | }
41 | ```
42 |
43 | ```html
44 |
45 | $title
46 | Data from the Server: {json_encode($post)}
47 |
48 |
{$post->Name}
49 |
50 | Id: {$post->Id}
51 |
52 |
53 | Version: {$post->Version}
54 |
55 |
56 |
57 | ```
58 |
59 | PostsAction middleware for handling requests to the API endpoint `/api/posts/{id}`
60 |
61 | ```php
62 | class PostsAction
63 | {
64 | public function __invoke(ServerRequestInterface $request) {
65 | $post = new PostModel();
66 | $post->Id = $request->getAttribute('params')['id'] ?? 0;
67 | $post->Name = 'Viewi ft. ReactPHP';
68 | $post->Version = 1;
69 | return new RawJsonResponse($post);
70 | }
71 | }
72 | ```
73 |
74 | ## Async SSR
75 |
76 | [http://localhost:8080/async-ssr-test/100500](http://localhost:8080/async-ssr-test/100500)
77 |
78 | This page demonstrates async server side rendering. It loads 4 posts in async mode.
79 |
80 | `PostsActionAsync` simulates I/O DB read with random timer value.
81 |
82 | 
83 |
84 | ```html
85 |
86 | $title
87 | This page loads 4 posts in non-blocking asynchronous mode.
88 |
89 |
90 |
91 |
92 |
93 | ```
94 |
95 | ```php
96 | http->get("/api/posts/{$this->id}/async")->then(
116 | function (PostModel $post) {
117 | $this->post = $post;
118 | // print_r(['$http->get->then->success', $post]);
119 | },
120 | function ($error) {
121 | echo $error;
122 | // print_r(['$http->get->then->error', $error]);
123 | }
124 | );
125 | }
126 | }
127 | ```
128 |
129 | `PostsActionAsync`
130 |
131 | ```php
132 | getAttribute('params')['ms'] ?? 50;
148 | if ($ms > 5000) // we don't want it to be more than 5 sec
149 | {
150 | $ms = 5000;
151 | }
152 | // Simulating I/O delay (DB/file read) with timer
153 | Loop::addTimer($ms / 1000 + (rand(1, 20) / 100), function () use ($resolve, $request) {
154 | $postId = $request->getAttribute('params')['id'] ?? 0;
155 | $post = new PostModel();
156 | $post->Id = $postId;
157 | $post->Name = "Viewi ft. ReactPHP $postId";
158 | $post->Version = $postId + 1000;
159 | $response = new RawJsonResponse($post);
160 | // echo "request Loop:1. \n";
161 | $resolve($response);
162 | });
163 | });
164 | }
165 | }
166 | ```
167 |
168 | ## Async Interceptors
169 |
170 | [http://localhost:8080/interceptors-test/100500](http://localhost:8080/interceptors-test/100500)
171 |
172 | This page demonstrates async interceptors during SSR
173 |
174 | 
175 |
176 | ```php
177 | http
200 | ->withInterceptor(SessionInterceptor::class)
201 | ->withInterceptor(AuthorizationInterceptor::class)
202 | ->get("/api/posts/{$this->id}/async/200")->then(
203 | function (PostModel $post) {
204 | $this->post = $post;
205 | },
206 | function ($error) {
207 | $this->message = $error;
208 | }
209 | );
210 | }
211 | }
212 | ```
213 |
214 | ## Async Middleware (Viewi IMiddleware)
215 |
216 | [http://localhost:8080/middleware-test/100500](http://localhost:8080/middleware-test/100500)
217 |
218 | This page demonstrates using guards in asynchronous mode
219 |
220 | 
221 |
222 | ```php
223 | http
248 | ->get("/api/posts/{$this->id}/async/1")->then(
249 | function (PostModel $post) {
250 | $this->post = $post;
251 | },
252 | function ($error) {
253 | $this->message = $error;
254 | }
255 | );
256 | }
257 | }
258 | ```
259 |
260 | ## Middleware Unauthorized example
261 |
262 | [http://localhost:8080/middleware-fail-test/100500](http://localhost:8080/middleware-fail-test/100500)
263 |
264 | This page should redirect you back to home page. Guard will check authorization and it won't pass.
265 |
266 | ## Counter
267 |
268 | [http://localhost:8080/counter](http://localhost:8080/counter)
269 |
270 | and
271 |
272 | [http://localhost:8080/counter/123](http://localhost:8080/counter/123)
273 |
274 | Demo of a handling click events in order to update the viewi with a new count value.
275 |
276 | 
277 |
278 | ```html
279 | -
280 | $count
281 | +
282 | ```
283 |
284 | Also it demonstrates how to inject route parameter into the component:
285 |
286 | `$router->get('/counter/{page}', CounterPage::class);`
287 |
288 | ```php
289 | page = $page;
302 | }
303 | }
304 | ```
305 |
306 | ## Todo
307 |
308 | [http://localhost:8080/todo](http://localhost:8080/todo)
309 |
310 | Demonstrates a small Todo application.
311 |
312 | 
313 |
314 | ## Redirect test
315 |
316 | [http://localhost:8080/redirect-test](http://localhost:8080/redirect-test)
317 |
318 | Demonstrates how to redirect to another page using `ClientRouter`
319 |
320 | ```php
321 | router->navigate('/');
337 | }
338 | }
339 | ```
340 |
341 | ## Curren Url page
342 |
343 | [http://localhost:8080/current-url](http://localhost:8080/current-url)
344 |
345 | This page demonstrates how to get a current url path of the page with `ClientRouter`
346 |
347 | ```php
348 | currentUrl = $router->getUrl();
362 | }
363 | }
364 | ```
365 |
366 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-present Ivan Voitovych
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "react/http": "^1.9",
4 | "viewi/viewi": "^2.0",
5 | "react/async": "^4.1"
6 | },
7 | "autoload": {
8 | "psr-4": {
9 | "App\\": "App/",
10 | "Components\\": "viewi-app/Components/"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/public/about.txt:
--------------------------------------------------------------------------------
1 | This favicon was generated using the following font:
2 |
3 | - Font Title: Leckerli One
4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli"
5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v14/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))
7 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/public/favicon.ico
--------------------------------------------------------------------------------
/public/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-size: 1em;
3 | }
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/screenshots/async.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/screenshots/async.png
--------------------------------------------------------------------------------
/screenshots/asyncInterceptors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/screenshots/asyncInterceptors.png
--------------------------------------------------------------------------------
/screenshots/asyncMiddleware.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/screenshots/asyncMiddleware.png
--------------------------------------------------------------------------------
/screenshots/counter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/screenshots/counter.png
--------------------------------------------------------------------------------
/screenshots/homePage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/screenshots/homePage.png
--------------------------------------------------------------------------------
/screenshots/todo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivanvoitovych/viewi-reactphp-demo/dd5056ee6e3c696570e1cfefa6bc77fd452502fa/screenshots/todo.png
--------------------------------------------------------------------------------
/server.php:
--------------------------------------------------------------------------------
1 | router();
21 | $viewiRequestHandler = new RequestsHandlerMiddleware($router, $viewiApp);
22 |
23 | $viewiReactBridge = new ViewiReactBridge($viewiRequestHandler);
24 | $app->factory()->add(IViewiBridge::class, function () use ($viewiReactBridge) {
25 | return $viewiReactBridge;
26 | });
27 |
28 |
29 | $router->register('get', '/api/posts/{id}', new PostsAction());
30 | $router->register('get', '/api/posts/{id}/async/{ms?}', new PostsActionAsync());
31 | $router->register('post', '/api/authorization/session', new AuthSessionAction());
32 | $router->register('post', '/api/authorization/token/{valid}', new AuthTokenAction());
33 |
34 | // include viewi routes
35 | include __DIR__ . '/viewi-app/routes.php';
36 |
37 | $http = new React\Http\HttpServer(
38 | new StaticFilesMiddleware(__DIR__ . '/public'),
39 | $viewiRequestHandler
40 | );
41 |
42 | $socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '127.0.0.1:8080');
43 | $http->listen($socket);
44 |
45 | echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL;
46 |
--------------------------------------------------------------------------------
/viewi-app/Components/Models/PostModel.php:
--------------------------------------------------------------------------------
1 | withHeader
23 | // call handle to continue with the request
24 | $this->http->post('/api/authorization/token/true')->then(function ($response) use ($request, $handler) {
25 | $newRequest = $request->withHeader('Authorization', $response['token']);
26 | $handler->next($newRequest);
27 | }, function ($error) use ($request, $handler) {
28 | $handler->reject($request);
29 | });
30 | }
31 |
32 | public function response(Response $response, IResponseHandler $handler)
33 | {
34 | // access or modify $response
35 | // call $handler->next if you are good with the response
36 | // call $handler->reject to reject the response
37 | if ($response->status === 0) {
38 | // rejected
39 | // make it Unauthorized
40 | $response->status = 401;
41 | $response->body = 'Unauthorized';
42 | }
43 | $handler->next($response);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/viewi-app/Components/Services/Interceptors/SessionInterceptor.php:
--------------------------------------------------------------------------------
1 | withHeader
23 | // call handle to continue with the request
24 | $this->http->post('/api/authorization/session')->then(function ($response) use ($request, $handler) {
25 | $newRequest = $request->withHeader('X-SESSION-ID', $response['session']);
26 | $handler->next($newRequest);
27 | }, function ($error) use ($request, $handler) {
28 | $handler->reject($request);
29 | });
30 | }
31 |
32 | public function response(Response $response, IResponseHandler $handler)
33 | {
34 | // access or modify $response
35 | // call $handler->next if you are good with the response
36 | // call $handler->reject to reject the response
37 | if ($response->status === 0) {
38 | // rejected
39 | // make it Unauthorized
40 | $response->status = 440;
41 | $response->body = 'Session has expired.';
42 | }
43 | $handler->next($response);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/viewi-app/Components/Services/Middleware/AuthGuard.php:
--------------------------------------------------------------------------------
1 | http->post('/api/authorization/token/true')->then(function ($response) use ($c) {
21 | // all good - call $c->next(); or $c->next(true);
22 | $c->next();
23 | }, function () use ($c) {
24 | // If we want to cancel - we call $c->next(false);
25 | $c->next(false);
26 | $this->router->navigate('/');
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/viewi-app/Components/Services/Middleware/AuthGuardFail.php:
--------------------------------------------------------------------------------
1 | http->post('/api/authorization/token/false')->then(function ($response) use ($c) {
21 | // all good - call $c->next(); or $c->next(true);
22 | $c->next();
23 | }, function () use ($c) {
24 | // If we want to cancel - we call $c->next(false);
25 | $c->next(false);
26 | $this->router->navigate('/');
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/viewi-app/Components/Services/Middleware/SessionGuard.php:
--------------------------------------------------------------------------------
1 | http->post('/api/authorization/session')->then(function ($response) use ($c) {
21 | // all good - call $c->next(); or $c->next(true);
22 | $c->next();
23 | }, function () use ($c) {
24 | // If we want to cancel - we call $c->next(false);
25 | $c->next(false);
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/viewi-app/Components/Services/Reducers/CounterReducer.php:
--------------------------------------------------------------------------------
1 | count++;
15 | }
16 |
17 | public function decrement()
18 | {
19 | $this->count--;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/viewi-app/Components/Services/Reducers/TodoReducer.php:
--------------------------------------------------------------------------------
1 | items = [...$this->items, $text];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/AsyncTest/AsyncTestComponent.html:
--------------------------------------------------------------------------------
1 |
2 | $title
3 | This page loads 4 posts in non-blocking asynchronous mode.
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/AsyncTest/AsyncTestComponent.php:
--------------------------------------------------------------------------------
1 | Data from the Server: {json_encode($post)}
2 |
3 |
{$post->Name}
4 |
5 | Id: {$post->Id}
6 |
7 |
8 | Version: {$post->Version}
9 |
10 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/AsyncTest/PostComponent.php:
--------------------------------------------------------------------------------
1 | http->get("/api/posts/{$this->id}/async")->then(
22 | function (PostModel $post) {
23 | $this->post = $post;
24 | // print_r(['$http->get->then->success', $post]);
25 | },
26 | function (Response $response) {
27 | echo $response->body;
28 | // print_r(['$http->get->then->error', $error]);
29 | }
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Common/MenuBar.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Common/MenuBar.php:
--------------------------------------------------------------------------------
1 | -
2 | $count
3 | +
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Counter/Counter.php:
--------------------------------------------------------------------------------
1 | count++;
14 | }
15 |
16 | public function decrement()
17 | {
18 | $this->count--;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Home/HomePage.html:
--------------------------------------------------------------------------------
1 |
2 | $title
3 | Data from the Server: {json_encode($post)}
4 |
5 |
{$post->Name}
6 |
7 | Id: {$post->Id}
8 |
9 |
10 | Version: {$post->Version}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Home/HomePage.php:
--------------------------------------------------------------------------------
1 | http->get('/api/posts/45')->then(
22 | function (PostModel $post) {
23 | $this->post = $post;
24 | },
25 | function (Response $response) {
26 | echo $response->body;
27 | }
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/InterceptorsTest/InterceptorsTestComponent.html:
--------------------------------------------------------------------------------
1 |
2 | $title
3 |
4 | On this page we call session API first using SessionInterceptor. And only if everything is fine we proceed with getting the Post.
5 |
6 |
7 | Expected response time 600 - 800 ms (randomly with Loop::addTimer)
8 |
9 |
10 |
{$post->Name}
11 |
12 | Id: {$post->Id}
13 |
14 |
15 | Version: {$post->Version}
16 |
17 |
18 |
19 | $message
20 |
21 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/InterceptorsTest/InterceptorsTestComponent.php:
--------------------------------------------------------------------------------
1 | http
25 | ->withInterceptor(SessionInterceptor::class)
26 | ->withInterceptor(AuthorizationInterceptor::class)
27 | ->get("/api/posts/{$this->id}/async/200")->then(
28 | function (PostModel $post) {
29 | $this->post = $post;
30 | },
31 | function (Response $response) {
32 | $this->message = $response->body;
33 | }
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Layouts/Layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | $title | Viewi
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Layouts/Layout.php:
--------------------------------------------------------------------------------
1 |
2 | $title
3 |
4 | On this page we use Middleware list (Guards) and guard should not allow as to open this page. Expected redirect
5 | to home page.
6 |
7 |
8 |
{$post->Name}
9 |
10 | Id: {$post->Id}
11 |
12 |
13 | Version: {$post->Version}
14 |
15 |
16 |
17 | $message
18 |
19 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/MiddlewareTest/MiddlewareFailTestComponent.php:
--------------------------------------------------------------------------------
1 | http
27 | ->get("/api/posts/{$this->id}/async/1")->then(
28 | function (PostModel $post) {
29 | $this->post = $post;
30 | },
31 | function (Response $response) {
32 | $this->message = $response->body;
33 | }
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/MiddlewareTest/MiddlewareTestComponent.html:
--------------------------------------------------------------------------------
1 |
2 | $title
3 |
4 | On this page we use Middleware list (Guards). And only if everything is fine the component will be rendered.
5 |
6 |
7 | Expected response time 400 - 600 ms (randomly with Loop::addTimer)
8 |
9 |
10 |
{$post->Name}
11 |
12 | Id: {$post->Id}
13 |
14 |
15 | Version: {$post->Version}
16 |
17 |
18 |
19 | $message
20 |
21 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/MiddlewareTest/MiddlewareTestComponent.php:
--------------------------------------------------------------------------------
1 | http
27 | ->get("/api/posts/{$this->id}/async/1")->then(
28 | function (PostModel $post) {
29 | $this->post = $post;
30 | },
31 | function (Response $response) {
32 | $this->message = $response->body;
33 | }
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/NotFound/NotFoundPage.html:
--------------------------------------------------------------------------------
1 |
2 | Page not found
3 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/NotFound/NotFoundPage.php:
--------------------------------------------------------------------------------
1 |
2 | Counter
3 | Route argument page = $page
4 |
5 |
6 |
7 |
Regular Counter
8 |
9 |
10 |
11 |
Counter with shared state
12 |
13 |
14 | Unlike the regular counter, this one remembers its state
15 | for further use. No matter how you change the count, it will keep it in reducer. And foremore - you
16 | can use it in different places (share with another component for example) and the count will be the
17 | same.
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Pages/CounterPage.php:
--------------------------------------------------------------------------------
1 | page = $page;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Pages/CurrentUrlTestPage.html:
--------------------------------------------------------------------------------
1 |
2 | Current Url is $currentUrl
3 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Pages/CurrentUrlTestPage.php:
--------------------------------------------------------------------------------
1 | currentUrl = $router->getUrl();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Pages/RedirectTestComponent.html:
--------------------------------------------------------------------------------
1 |
2 | You should never see me
3 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Pages/RedirectTestComponent.php:
--------------------------------------------------------------------------------
1 | router->navigate('/');
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Pages/TodoAppPage.html:
--------------------------------------------------------------------------------
1 |
2 | Todo
3 |
4 |
5 |
6 |
Regular Todo
7 |
8 |
9 |
10 |
Todo with shared state
11 |
12 |
13 | Unlike the regular todo, this one remembers its state for
14 | further use. No matter how you change the todo list, it will keep it in reducer. And foremore - you
15 | can use it in different places (share with another component for example) and the list will be the
16 | same.
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/Pages/TodoAppPage.php:
--------------------------------------------------------------------------------
1 | decrement" class="mui-btn mui-btn--accent">-
2 | {$counter->count}
3 | increment" class="mui-btn mui-btn--accent">+
--------------------------------------------------------------------------------
/viewi-app/Components/Views/StatefulCounter/StatefulCounter.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | What needs to be done?
5 |
6 |
7 | Add #{count($todo->items) + 1}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/StatefulTodoApp/StatefulTodoApp.php:
--------------------------------------------------------------------------------
1 | todo = $reducer;
17 | }
18 |
19 | public function handleSubmit(DomEvent $event)
20 | {
21 | $event->preventDefault();
22 | if (strlen($this->text) === 0) {
23 | return;
24 | }
25 | $this->todo->addNewItem($this->text);
26 | $this->text = '';
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/TodoApp/TodoApp.html:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/TodoApp/TodoApp.php:
--------------------------------------------------------------------------------
1 | preventDefault();
16 | if (strlen($this->text) == 0) {
17 | return;
18 | }
19 | $this->items = [...$this->items, $this->text];
20 | $this->text = '';
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/TodoApp/TodoList.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/viewi-app/Components/Views/TodoApp/TodoList.php:
--------------------------------------------------------------------------------
1 | build();
13 | echo $logs;
14 |
--------------------------------------------------------------------------------
/viewi-app/config.php:
--------------------------------------------------------------------------------
1 | buildTo($buildPath)
16 | ->buildFrom($componentsPath)
17 | ->withJsEntry($jsPath)
18 | ->putAssetsTo($publicPath)
19 | ->assetsPublicUrl($assetsPublicUrl)
20 | ->withAssets($assetsSourcePath)
21 | ->combine(false)
22 | ->minify(false)
23 | ->developmentMode(true)
24 | ->buildJsSourceCode()
25 | ->watchWithNPM(true);
26 |
--------------------------------------------------------------------------------
/viewi-app/js/build.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import * as esbuild from 'esbuild'
3 | import * as chokidar from 'chokidar';
4 | import anymatch from 'anymatch';
5 | import path from 'path';
6 | import { exec } from 'node:child_process';
7 | import { PurgeCSS } from 'purgecss';
8 | import { writeFile } from 'node:fs/promises';
9 | import { lazyGroups } from './app/lazyGroups.mjs';
10 | import { buildActions } from './app/buildActions.mjs';
11 | import { readFile } from 'fs/promises';
12 |
13 | let watchMode = false;
14 |
15 | process.argv.forEach(function (val, index, array) {
16 | switch (val) {
17 | case '--watch': {
18 | watchMode = true;
19 | break;
20 | }
21 | default:
22 | break; // noop
23 | }
24 | });
25 |
26 | const runBuild = async function () {
27 | const base = {
28 | entryPoints: ['./viewi/index.ts'],
29 | logLevel: "info",
30 | treeShaking: true,
31 | bundle: true,
32 | };
33 |
34 | await esbuild.build({ ...base, outfile: './dist/viewi.js' });
35 | await esbuild.build({ ...base, minify: true, outfile: './dist/viewi.min.js' });
36 |
37 | for (let group in lazyGroups) {
38 | const entry = lazyGroups[group];
39 | await esbuild.build({ ...base, entryPoints: [entry], outfile: `./dist/viewi.${group}.js` });
40 | await esbuild.build({ ...base, entryPoints: [entry], minify: true, outfile: `./dist/viewi.${group}.min.js` });
41 | }
42 |
43 | // build actions
44 | for (let i = 0; i < buildActions.items.length; i++) {
45 | const buildItem = buildActions.items[i];
46 | switch (buildItem.type) {
47 | case 'css': {
48 | const cssItems = buildItem.data.links.map(x => './../assets' + x);
49 | const buildList = [];
50 | if (buildItem.data.combine) {
51 | buildList.push(cssItems);
52 | } else {
53 | for (let c = 0; c < cssItems.length; c++) {
54 | buildList.push([cssItems[c]]);
55 | }
56 | }
57 | for (let c = 0; c < buildList.length; c++) {
58 | const entries = buildList[c];
59 | let combinedCss = '';
60 | if (buildItem.data.purge) {
61 | const purgeCSSResult = await new PurgeCSS().purge({
62 | content: ['./../**/*.js', './../**/*.php', './../**/*.html'],
63 | css: entries,
64 | skippedContentGlobs: ['**/node_modules/**', '**/build/**']
65 | });
66 | for (let i = 0; i < purgeCSSResult.length; i++) {
67 | const purgedCSS = purgeCSSResult[i];
68 | combinedCss += purgedCSS.css;
69 | }
70 | } else {
71 | for (let f = 0; f < entries.length; f++) {
72 | combinedCss += await readFile(entries[f]);
73 | }
74 | }
75 | await esbuild.build({
76 | stdin: {
77 | contents: combinedCss,
78 | // These are all optional:
79 | resolveDir: './../assets',
80 | loader: 'css'
81 | },
82 | // entryPoints: buildItem.data.links.map(x => './../assets' + x),
83 | bundle: true,
84 | // loader: { '.png': 'copy', '.jpg': 'copy' },
85 | external: ['*.png', '*.jpg'],
86 | minify: !!buildItem.data.minify,
87 | // outdir: './dist/assets',
88 | outfile: './dist/assets' + (buildItem.data.combine ? buildItem.data.output : buildItem.data.links[c]),
89 | });
90 | }
91 | break;
92 | }
93 | default: {
94 | console.warn(`Type action ${buildItem.type} is not implemented.`);
95 | break
96 | }
97 | }
98 | }
99 | };
100 |
101 | const runServerBuild = function () {
102 | // exec php viewi build
103 | return new Promise(function (resolve, reject) {
104 | exec('php ./../build.php', (error, stdout, stderr) => {
105 | if (error) {
106 | reject(`exec error: ${error} \n${stdout}${stderr}`);
107 | return;
108 | }
109 | console.log(`PHP build: ${stdout}`);
110 | if (stderr) {
111 | console.error(`PHP build error: ${stderr}`);
112 | }
113 | resolve();
114 | });
115 | });
116 | };
117 |
118 | const runBuildAll = async function () {
119 | console.log('Running build..');
120 | try {
121 | await runServerBuild();
122 | } catch (ex) {
123 | console.error(ex);
124 | }
125 | // await runBuild();
126 | };
127 |
128 | let buildTimer = 0;
129 |
130 | const runWatch = async function () {
131 | const ignored = ['**/build/**', '/js/dist/**', '/js/viewi/**', '**/node_modules/**', '**/app/**', '**/combined.css'];
132 | // https://github.com/paulmillr/chokidar
133 | chokidar.watch(['.\\..\\']).on('all', (event, itemPath) => {
134 | const normalizedPath = path.normalize(itemPath).replace(/\\/g, '/').replace('..', '');
135 | // https://www.npmjs.com/package/anymatch
136 | if (!anymatch(ignored, normalizedPath)) {
137 | // console.log(event, normalizedPath);
138 | if (buildTimer) {
139 | clearTimeout(buildTimer);
140 | }
141 | buildTimer = setTimeout(runBuildAll, 200);
142 | }
143 | });
144 | };
145 |
146 | if (watchMode) {
147 | runWatch();
148 | } else {
149 | runBuild();
150 | }
151 |
--------------------------------------------------------------------------------
/viewi-app/js/modules/main/index.ts:
--------------------------------------------------------------------------------
1 | export const modules = {};
--------------------------------------------------------------------------------
/viewi-app/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "viewi-app",
3 | "version": "1.0.0",
4 | "description": "Reactive web application with Viewi",
5 | "license": "MIT",
6 | "engines": {
7 | "node": ">=12"
8 | },
9 | "scripts": {
10 | "build": "node ./build.mjs",
11 | "watch": "node ./build.mjs --watch"
12 | },
13 | "dependencies": {
14 | "anymatch": "^3.1.3",
15 | "chokidar": "^3.5.3",
16 | "esbuild": "0.18.17",
17 | "purgecss": "^5.0.0"
18 | }
19 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/anchor/anchor.ts:
--------------------------------------------------------------------------------
1 | import { HtmlNodeType } from "../node/htmlNodeType"
2 |
3 | export type Anchor = {
4 | target: HtmlNodeType,
5 | current: number,
6 | added: number,
7 | invalid: number[]
8 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/anchor/anchors.ts:
--------------------------------------------------------------------------------
1 | import { Anchor } from "./anchor";
2 |
3 | export const anchors: { [key: string]: Anchor } = {};
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/anchor/createAnchorNode.ts:
--------------------------------------------------------------------------------
1 | import { Anchor } from "./anchor";
2 | import { NodeAnchor } from "./nodeAnchor";
3 | import { TextAnchor } from "./textAnchor";
4 |
5 | let anchorNodeId = 0;
6 |
7 | export function nextAnchorNodeId(): number {
8 | return ++anchorNodeId;
9 | }
10 |
11 | export function createAnchorNode(target: NodeAnchor, insert: boolean = false, anchor?: Anchor, name?: string): TextAnchor {
12 | const anchorNode = document.createTextNode('') as TextAnchor;
13 | anchorNode._anchor = name ?? ('#' + ++anchorNodeId);
14 | if (anchor) {
15 | anchor.current++;
16 | }
17 | (insert || (anchor && target.childNodes.length > anchor.current))
18 | ? (anchor ? target : target.parentElement)!.insertBefore(anchorNode, anchor ? target.childNodes[anchor.current] : target)
19 | : target.appendChild(anchorNode);
20 | return anchorNode;
21 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/anchor/foreachAnchorEnum.ts:
--------------------------------------------------------------------------------
1 | export enum ForeachAnchorEnum {
2 | BeginAnchor = 'b',
3 | EndAnchor = 'e'
4 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/anchor/getAnchor.ts:
--------------------------------------------------------------------------------
1 | import { Anchor } from "./anchor";
2 | import { anchors } from "./anchors";
3 | import { NodeAnchor } from "./nodeAnchor";
4 |
5 | let anchorId = 0;
6 |
7 | export function getAnchor(target: NodeAnchor): Anchor {
8 | if (!target.__aid) {
9 | target.__aid = ++anchorId;
10 | anchors[target.__aid] = { current: -1, target, invalid: [], added: 0 };
11 | }
12 | return anchors[target.__aid];
13 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/anchor/nodeAnchor.ts:
--------------------------------------------------------------------------------
1 | export type NodeAnchor = Node & { __aid?: number };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/anchor/textAnchor.ts:
--------------------------------------------------------------------------------
1 | export type TextAnchor = Text & { _anchor?: string, previousSibling: (ChildNode & TextAnchor), nextSibling: (ChildNode & TextAnchor) };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/component/baseComponent.ts:
--------------------------------------------------------------------------------
1 | import { ReactiveProxy } from "../reactivity/makeProxy";
2 |
3 | export abstract class BaseComponent {
4 | __id: string = '';
5 | _props: { [key: string]: any } = {};
6 | $_callbacks: { [key: string]: Function } = {};
7 | _refs: { [key: string]: Node | BaseComponent } = {};
8 | _slots: { [key: string]: any } = {};
9 | _element: Node | null = null;
10 | $$t: Function[] = []; // template inline expressions
11 | $$r: { [key: string]: { [key: string]: [Function, any[]] } } = {}; // reactivity callbacks
12 | $$p: [trackerId: string, activated: ReactiveProxy][] = []; // shared reactivity track ids
13 | $: T;
14 | _provides: Object;
15 | _parent: null | BaseComponent = null;
16 | _name: string = 'BaseComponent';
17 |
18 | emitEvent(name: string, event?: any) {
19 | if (name in this.$_callbacks) {
20 | this.$_callbacks[name](event);
21 | }
22 | }
23 |
24 | provide(key: string, value: any): void {
25 | if (this._provides === this._parent?._provides) {
26 | this._provides = Object.create(this._provides);
27 | }
28 | this._provides[key] = value;
29 | }
30 |
31 | inject(key: string): any {
32 | return this._provides[key] || null;
33 | }
34 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/component/componentMetaData.ts:
--------------------------------------------------------------------------------
1 | import { ScopeType } from "../di/scopeType"
2 | import { TemplateNode } from "../node/templateNode"
3 |
4 | export type ComponentMetaData = {
5 | nodes?: TemplateNode,
6 | dependencies?: any[],
7 | di?: ScopeType,
8 | diProps?: {
9 | [key: string]: {
10 | name: string,
11 | di: ScopeType
12 | }
13 | }
14 | base?: boolean,
15 | custom?: boolean,
16 | renderer?: boolean,
17 | refs?: { [key: string]: boolean },
18 | parent?: string,
19 | lazy?: string,
20 | middleware?: string[],
21 | hooks?: {
22 | init?: boolean,
23 | mount?: boolean,
24 | mounted?: boolean,
25 | rendered?: boolean,
26 | destroy?: boolean
27 | }
28 | }
29 |
30 | export type ComponentMetaDataList = {
31 | [key: string]: ComponentMetaData
32 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/component/componentsJson.ts:
--------------------------------------------------------------------------------
1 | import { RouteItem } from "../router/routeItem"
2 | import { ComponentMetaDataList } from "./componentMetaData"
3 |
4 | export type ComponentsJson = {
5 | _meta: { boolean: string },
6 | _routes: RouteItem[],
7 | _config: { [key: string]: any },
8 | _startup: string[],
9 | _globals: { [key: string]: string }
10 | } & ComponentMetaDataList;
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/component/componentsMeta.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "../router/router";
2 | import { ComponentMetaDataList } from "./componentMetaData";
3 |
4 | export const componentsMeta: {
5 | list: ComponentMetaDataList,
6 | router: Router,
7 | config: { [key: string]: any },
8 | booleanAttributes: {},
9 | globals: { [key: string]: string }
10 | } = {
11 | list: {},
12 | config: {},
13 | globals: {},
14 | booleanAttributes: {},
15 | router: new Router()
16 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/component/iStartUp.ts:
--------------------------------------------------------------------------------
1 | export interface IStartUp {
2 | setUp(): void;
3 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/component/isComponent.ts:
--------------------------------------------------------------------------------
1 | import { componentsMeta } from "./componentsMeta";
2 |
3 | export function isComponent(name: string) {
4 | return (name in componentsMeta.list);
5 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/component/makeGlobal.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "./baseComponent";
2 | import { componentsMeta } from "./componentsMeta";
3 | import { makeGlobalMethod } from "./makeGlobalMethod";
4 |
5 | export function makeGlobal() {
6 | for (let key in componentsMeta.globals) {
7 | BaseComponent.prototype[key] = function () {
8 | BaseComponent.prototype[key] = makeGlobalMethod(key, componentsMeta.globals[key]);
9 | return BaseComponent.prototype[key].apply(null, arguments);
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/component/makeGlobalMethod.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "../di/resolve";
2 |
3 | export function makeGlobalMethod(method: string, typeName: string) {
4 | const instance = resolve(typeName);
5 | return function () {
6 | return instance[method].apply(instance, arguments);
7 | }
8 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/component/reserverProps.ts:
--------------------------------------------------------------------------------
1 | export const ReserverProps = {
2 | _props: true,
3 | $_callbacks: true,
4 | _refs: true,
5 | _slots: true,
6 | _element: true,
7 | $$t: true,
8 | $$r: true,
9 | $: true,
10 | $$p: true,
11 | _name: true,
12 | emitEvent: true,
13 | _provides: true,
14 | _parent: true,
15 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/di/delay.ts:
--------------------------------------------------------------------------------
1 | const delayedQueue = {};
2 |
3 | export const delay = {
4 | postpone: function (name: string, callback: Function) {
5 | delayedQueue[name] = callback;
6 | },
7 | ready: function (name: string) {
8 | if (!(name in delayedQueue)) {
9 | throw new Error("There is no postponed action for " + name);
10 | }
11 | delayedQueue[name]();
12 | delete delayedQueue[name];
13 | },
14 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/di/diContainer.ts:
--------------------------------------------------------------------------------
1 | export type DIContainer = { [key: string]: any };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/di/factory.ts:
--------------------------------------------------------------------------------
1 | import { components } from "../../../app/main/components";
2 | import { register } from "./register";
3 |
4 | type Constructor = new (...args: any[]) => any;
5 |
6 | export const factoryContainer: { [name: string]: () => T } = {};
7 |
8 | export function factory(name: string, implementation: T, factory: () => InstanceType) {
9 | register[name] = implementation;
10 | components[name] = implementation;
11 | factoryContainer[name] = factory;
12 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/di/globalScope.ts:
--------------------------------------------------------------------------------
1 | import { TextAnchor } from "../anchor/textAnchor"
2 | import { BaseComponent } from "../component/baseComponent"
3 | import { ContextScope } from "../lifecycle/contextScope"
4 | import { DIContainer } from "./diContainer"
5 |
6 | type RenderIteration = {
7 | instance: BaseComponent,
8 | scope: ContextScope,
9 | slots: { [key: string]: TextAnchor }
10 | }
11 |
12 | type GlobalScope = {
13 | hydrate: boolean,
14 | rootScope: ContextScope | false,
15 | scopedContainer: DIContainer,
16 | located: { [key: string]: boolean },
17 | iteration: { [key: string]: RenderIteration },
18 | lastIteration: { [key: string]: RenderIteration },
19 | layout: string,
20 | cancel: boolean
21 | }
22 |
23 | export const globalScope: GlobalScope = {
24 | hydrate: true, // first time hydrate, TODO: configurable, MFE won't need hydration
25 | rootScope: false,
26 | scopedContainer: {},
27 | located: {},
28 | iteration: {},
29 | lastIteration: {},
30 | layout: '',
31 | cancel: false
32 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/di/register.ts:
--------------------------------------------------------------------------------
1 | import { resources } from "../../../app/main/resources";
2 |
3 | export const register: { [name: string]: any } = window.ViewiApp ? window.ViewiApp[resources.name].register : {};
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/di/resolve.ts:
--------------------------------------------------------------------------------
1 | import { components } from "../../../app/main/components";
2 | import { BaseComponent } from "../component/baseComponent";
3 | import { componentsMeta } from "../component/componentsMeta";
4 | import { getScopeState } from "../lifecycle/scopeState";
5 | import { DIContainer } from "./diContainer";
6 | import { factoryContainer } from "./factory";
7 | import { globalScope } from "./globalScope";
8 | import { ScopeType } from "./scopeType";
9 |
10 | const singletonContainer: DIContainer = {};
11 | let nextInstanceId = 0;
12 | const rootProvides = {};
13 |
14 | export function resolve(name: string, params: { [key: string]: any } = {}, canBeNull: boolean = false, parent: BaseComponent | null = null) {
15 | if (!(name in componentsMeta.list)) {
16 | if (canBeNull) {
17 | return null;
18 | }
19 | throw new Error("Can't resolve " + name);
20 | }
21 | const info = componentsMeta.list[name];
22 | let instance: any = null;
23 | let container: boolean | DIContainer = false;
24 | if (info.di === "SINGLETON") {
25 | container = singletonContainer;
26 | } else if (info.di === "SCOPED") {
27 | container = globalScope.scopedContainer;
28 | }
29 | if (container && (name in container)) {
30 | // console.log('Returning from cache', name, container[name]);
31 | return container[name];
32 | }
33 | const toProvide = {};
34 | if (info.custom) {
35 | instance = factoryContainer[name]();
36 | } else if (!info.dependencies) {
37 | instance = new components[name]();
38 | } else {
39 | const constructArguments: any[] = [];
40 | for (let i in info.dependencies) {
41 | const dependency = info.dependencies[i];
42 | const diType = dependency['di'] || false;
43 | const argCanBeNull = !!dependency.null;
44 | var argument: any = null; // d.null
45 | if (diType === 'PARENT') {
46 | argument = parent ? parent.inject(dependency.name) : (rootProvides[dependency.name] || null);
47 | } else if (params && (dependency.argName in params)) {
48 | argument = params[dependency.argName];
49 | }
50 | else if (dependency.default) {
51 | argument = dependency.default; // TODO: copy object or array
52 | } else if (dependency.builtIn) {
53 | argument = dependency.name === 'string' ? '' : 0;
54 | } else {
55 | argument = resolve(dependency.name, {}, argCanBeNull, parent);
56 | }
57 | if (diType === 'COMPONENT') {
58 | toProvide[dependency.name] = argument;
59 | }
60 | constructArguments.push(argument);
61 | }
62 | instance = new components[name](...constructArguments);
63 | }
64 | if (info.base) {
65 | const baseComponent = instance as BaseComponent;
66 | baseComponent.__id = ++nextInstanceId + '';
67 | if (parent !== null) {
68 | baseComponent._provides = parent._provides;
69 | baseComponent._parent = parent;
70 | } else {
71 | baseComponent._provides = rootProvides;
72 | }
73 | for (let p in toProvide) {
74 | baseComponent.provide(p, toProvide[p]);
75 | }
76 | }
77 |
78 | // DI Props
79 | if (info.diProps) {
80 | for (let prop in info.diProps) {
81 | const dependency = info.diProps[prop];
82 | const diType = dependency.di;
83 | let propInstance = null;
84 | if (diType === 'PARENT') {
85 | propInstance = parent ? parent.inject(dependency.name) : (rootProvides[dependency.name] || null);
86 | } else if (diType === 'SINGLETON') {
87 | if (!(dependency.name in singletonContainer)) {
88 | propInstance = resolve(dependency.name, {}, false, parent);
89 | singletonContainer[dependency.name] = propInstance;
90 | }
91 | propInstance = singletonContainer[dependency.name];
92 | } else if (diType === 'SCOPED') {
93 | if (!(dependency.name in globalScope.scopedContainer)) {
94 | propInstance = resolve(dependency.name, {}, false, parent);
95 | globalScope.scopedContainer[dependency.name] = propInstance;
96 | }
97 | propInstance = globalScope.scopedContainer[dependency.name];
98 | } else {
99 | propInstance = resolve(dependency.name, {}, false, parent);
100 | }
101 | instance[prop] = propInstance;
102 | if (diType === 'COMPONENT') {
103 | (instance as BaseComponent).provide(dependency.name, propInstance);
104 | }
105 | }
106 | }
107 |
108 | const scopeState = getScopeState();
109 | if (scopeState.state[name]) {
110 | for (let prop in scopeState.state[name]) {
111 | instance[prop] = scopeState.state[name][prop];
112 | }
113 | }
114 | if (container) {
115 | container[name] = instance;
116 | }
117 | return instance;
118 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/di/scopeType.ts:
--------------------------------------------------------------------------------
1 | export type ScopeType = 'SINGLETON' | 'SCOPED' | 'TRANSIENT' | 'COMPONENT' | 'PARENT';
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/di/setUp.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { IStartUp } from "../component/iStartUp";
3 | import { Platform } from "../environment/platform";
4 | import { HttpClient } from "../http/httpClient";
5 | import { Portal } from "../portal/portal";
6 | import { factory } from "./factory";
7 | import { register } from "./register";
8 | import { resolve } from "./resolve";
9 |
10 | export function setUp(startUpItems: string[]) {
11 | register['BaseComponent'] = BaseComponent;
12 | factory('HttpClient', HttpClient, () => new HttpClient());
13 | factory('Platform', Platform, () => new Platform());
14 | factory('Portal', Portal, () => new Portal());
15 | for (let i = 0; i < startUpItems.length; i++) {
16 | const stratUpInstance = resolve(startUpItems[i]) as IStartUp;
17 | stratUpInstance.setUp();
18 | }
19 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/directive/conditionalDirective.ts:
--------------------------------------------------------------------------------
1 | export type ConditionalDirective = {
2 | values: boolean[],
3 | subs: string[],
4 | index: number
5 | }
6 |
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/directive/directive.ts:
--------------------------------------------------------------------------------
1 | import { DirectiveType } from "./directiveType"
2 |
3 | export type Directive = {
4 | type: DirectiveType
5 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/directive/directiveMap.ts:
--------------------------------------------------------------------------------
1 | export type DirectiveMap = {
2 | map: { [key: number]: boolean },
3 | storage: { [key: string]: any }
4 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/directive/directiveStorageType.ts:
--------------------------------------------------------------------------------
1 | export enum DirectiveStorageType {
2 | Condition = 'conditions',
3 | Foreach = 'foreach'
4 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/directive/directiveType.ts:
--------------------------------------------------------------------------------
1 | export type DirectiveType = 'if' | 'else-if' | 'else' | 'foreach';
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/environment/platform.ts:
--------------------------------------------------------------------------------
1 | import { componentsMeta } from "../component/componentsMeta";
2 | import { handleUrl, onUrlUpdate } from "../router/handleUrl";
3 |
4 | class Platform {
5 | browser: true = true;
6 | server: false = false;
7 |
8 | constructor() {
9 | }
10 |
11 | getConfig() {
12 | return componentsMeta.config;
13 | }
14 |
15 | redirect(url: string) {
16 | handleUrl(url);
17 | }
18 |
19 | navigateBack() {
20 | history.back();
21 | }
22 |
23 | getCurrentUrl(): string {
24 | return location.pathname + location.search;
25 | }
26 |
27 | setResponseStatus(status: number): void {
28 | // server side only
29 | }
30 |
31 | getCurrentUrlPath(): string {
32 | return location.pathname;
33 | }
34 |
35 | getQueryParams() {
36 | return Object.fromEntries(new URLSearchParams(location.search));
37 | }
38 |
39 | onUrlUpdate(callback: Function) {
40 | onUrlUpdate.callback = callback;
41 | }
42 | }
43 |
44 | export { Platform }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/events/resolver.ts:
--------------------------------------------------------------------------------
1 | export type ResolverAction = (callback: (result: any, error?: any) => void) => void;
2 |
3 | class Resolver {
4 | onSuccess: CallableFunction;
5 | onError: CallableFunction | null = null;
6 | onAlways: CallableFunction | null = null;
7 | result = null;
8 | lastError = null;
9 | action: ResolverAction;
10 |
11 | constructor(action: ResolverAction) {
12 | this.action = action;
13 | }
14 |
15 | error(onError: CallableFunction) {
16 | this.onError = onError;
17 | }
18 |
19 | success(onSuccess: CallableFunction) {
20 | this.onSuccess = onSuccess;
21 | }
22 |
23 | always(always: CallableFunction) {
24 | this.onAlways = always;
25 | }
26 |
27 | run() {
28 | const $this = this;
29 | this.action(function (result: any, error: any) {
30 | $this.result = result;
31 | let throwError = false;
32 | if (error) {
33 | $this.lastError = error;
34 | if ($this.onError !== null) {
35 | $this.onError(error);
36 | }
37 | else {
38 | throwError = true;
39 | }
40 | } else {
41 | $this.onSuccess($this.result);
42 | }
43 | if ($this.onAlways != null) {
44 | $this.onAlways();
45 | }
46 | if (throwError) {
47 | throw $this.lastError;
48 | }
49 | });
50 | }
51 |
52 | then(onSuccess: CallableFunction, onError?: CallableFunction, always?: CallableFunction) {
53 | this.onSuccess = onSuccess;
54 | if (onError) {
55 | this.onError = onError;
56 | }
57 | if (always) {
58 | this.onAlways = always;
59 | }
60 | this.run();
61 | }
62 | }
63 |
64 | export { Resolver }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/helpers/ensureType.ts:
--------------------------------------------------------------------------------
1 | export const ensureTypeOptions: {
2 | enable: boolean
3 | } = {
4 | enable: false
5 | }
6 |
7 | export function ensureType(targetType: Function, obj: object) {
8 | if (ensureTypeOptions.enable && Object.getPrototypeOf(obj) !== targetType.prototype) {
9 | ensureTypeOptions.enable = false;
10 | Object.setPrototypeOf(obj, targetType.prototype);
11 | }
12 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/helpers/isBlob.ts:
--------------------------------------------------------------------------------
1 | export function isBlob(data: any) {
2 | if ('Blob' in window && data instanceof Blob) {
3 | return true;
4 | }
5 | return false;
6 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/helpers/isSvg.ts:
--------------------------------------------------------------------------------
1 | const svgMap = {};
2 | const svgTagsString =
3 | 'svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,' +
4 | 'defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,' +
5 | 'feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,' +
6 | 'feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,' +
7 | 'feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,' +
8 | 'fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,' +
9 | 'foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,' +
10 | 'mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,' +
11 | 'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' +
12 | 'text,textPath,title,tspan,unknown,use,view';
13 |
14 | const svgTagsList = svgTagsString.split(',');
15 |
16 | for (let i = 0; i < svgTagsList.length; i++) {
17 | svgMap[svgTagsList[i]] = true;
18 | }
19 |
20 | export function isSvg(tag: string) {
21 | return (tag.toLowerCase() in svgMap);
22 | }
23 |
24 | export const xLinkNs = "http://www.w3.org/1999/xlink";
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/helpers/svgNameSpace.ts:
--------------------------------------------------------------------------------
1 | export const svgNameSpace = 'http://www.w3.org/2000/svg';
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/http/httpClient.ts:
--------------------------------------------------------------------------------
1 | import { Resolver } from "../events/resolver";
2 | import { getScopeState } from "../lifecycle/scopeState";
3 | import { MethodType } from "./methodType";
4 | import { runRequest } from "./runRequest";
5 | import { Response } from "./response";
6 | import { Request } from "./request";
7 | import { resolve } from "../di/resolve";
8 | import { IHttpInterceptor } from "./iHttpInterceptor";
9 | import { IRequestHandler } from "./iRequestHandler";
10 | import { IResponseHandler } from "./iResponseHandler";
11 |
12 | const interceptResponses = function (response: Response, callback, interceptorInstances: IHttpInterceptor[]) {
13 | const total = interceptorInstances.length;
14 | let current = total;
15 |
16 | const lastCall = function (response: Response, keepGoing: boolean) {
17 | if (keepGoing && response.status >= 200 && response.status < 300) {
18 | callback(response.body);
19 | } else {
20 | callback(undefined, response);
21 | }
22 | };
23 |
24 | const run = function (response: Response, keepGoing: boolean) {
25 | if (keepGoing) {
26 | if (current > -1) {
27 | const interceptor: IHttpInterceptor = interceptorInstances[current];
28 | interceptor.response(response, responseHandler);
29 | } else {
30 | lastCall(response, keepGoing);
31 | }
32 | } else {
33 | lastCall(response, keepGoing);
34 | }
35 | };
36 |
37 | const responseHandler: IResponseHandler = {
38 | next: function (response: Response) {
39 | current--;
40 | run(response, true);
41 | },
42 | reject: function (response: Response) {
43 | current--;
44 | run(response, false);
45 | }
46 | };
47 | responseHandler.next(response);
48 | }
49 |
50 | class HttpClient {
51 | interceptors: string[] = [];
52 |
53 | request(method: MethodType, url: string, body?: any, headers?: { [name: string]: string }) {
54 | const $this = this;
55 | const resolver = new Resolver(function (callback) {
56 | try {
57 | const state = getScopeState();
58 | const request = new Request(url, method, headers, body);
59 | let current = -1;
60 | const total = $this.interceptors.length;
61 | const interceptorInstances: IHttpInterceptor[] = [];
62 | const lastCall = function (request: Request, keepGoing: boolean) {
63 | if (keepGoing) {
64 | const requestKey = request.method + '_' + request.url + '_' + JSON.stringify(request.body);
65 | if (requestKey in state.http) {
66 | const responseData: { status: number, data: any } = JSON.parse(state.http[requestKey]);
67 | delete state.http[requestKey];
68 | const response = new Response(request.url, responseData.status, 'OK', {}, responseData.data);
69 | interceptResponses(response, callback, interceptorInstances);
70 | return;
71 | } else {
72 | runRequest(function (response: Response) {
73 | interceptResponses(response, callback, interceptorInstances);
74 | }, request.method, request.url, request.body, request.headers);
75 | }
76 | } else {
77 | const response = new Response(request.url, 0, 'Rejected', {}, null);
78 | interceptResponses(response, callback, interceptorInstances);
79 | }
80 | }
81 | const run = function (request: Request, keepGoing: boolean) {
82 | if (!keepGoing) {
83 | lastCall(request, keepGoing);
84 | return;
85 | }
86 | if (current < total) {
87 | const interceptor: IHttpInterceptor = resolve($this.interceptors[current]);
88 | interceptorInstances.push(interceptor);
89 | interceptor.request(request, requestHandler);
90 | } else {
91 | lastCall(request, keepGoing);
92 | }
93 | };
94 | const requestHandler: IRequestHandler = {
95 | next: function (request: Request): void {
96 | current++;
97 | run(request, true);
98 | },
99 | reject: function (request: Request): void {
100 | current++;
101 | run(request, false);
102 | }
103 | };
104 | requestHandler.next(request);
105 | } catch (ex) {
106 | console.error(ex);
107 | callback(undefined, ex);
108 | }
109 | });
110 |
111 | return resolver;
112 | }
113 |
114 | get(url: string, headers?: { [name: string]: string }) {
115 | return this.request("get", url, null, headers);
116 | }
117 |
118 | post(url: string, body?: any, headers?: { [name: string]: string }) {
119 | return this.request("post", url, body, headers);
120 | }
121 |
122 | put(url: string, body?: any, headers?: { [name: string]: string }) {
123 | return this.request("put", url, body, headers);
124 | }
125 |
126 | delete(url: string, body?: any, headers?: { [name: string]: string }) {
127 | return this.request("delete", url, body, headers);
128 | }
129 |
130 | patch(url: string, body?: any, headers?: { [name: string]: string }) {
131 | return this.request("patch", url, body, headers);
132 | }
133 |
134 | withInterceptor(interceptor: string) {
135 | const http = new HttpClient();
136 | http.interceptors = [...this.interceptors, interceptor];
137 | return http;
138 | }
139 | }
140 |
141 | export { HttpClient }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/http/iHttpInterceptor.ts:
--------------------------------------------------------------------------------
1 | import { IRequestHandler } from "./iRequestHandler";
2 | import { IResponseHandler } from "./iResponseHandler";
3 | import { Request } from "./request";
4 | import { Response } from "./response";
5 |
6 | export type IHttpInterceptor = {
7 | request(request: Request, handler: IRequestHandler): void;
8 | response(response: Response, handler: IResponseHandler): void;
9 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/http/iRequestHandler.ts:
--------------------------------------------------------------------------------
1 | import { Request } from "./request";
2 |
3 | export type IRequestHandler = {
4 | next(request: Request): void;
5 | reject(request: Request): void;
6 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/http/iResponseHandler.ts:
--------------------------------------------------------------------------------
1 | import { Response } from "./response";
2 |
3 | export type IResponseHandler = {
4 | next(response: Response): void;
5 | reject(response: Response): void;
6 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/http/injectScript.ts:
--------------------------------------------------------------------------------
1 | export function injectScript(url: string) {
2 | const head = document.head;
3 | const script = document.createElement('script');
4 | script.type = 'text/javascript';
5 | script.src = url;
6 | head.appendChild(script);
7 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/http/methodType.ts:
--------------------------------------------------------------------------------
1 | export type MethodType = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'head'
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/http/request.ts:
--------------------------------------------------------------------------------
1 | import { MethodType } from "./methodType";
2 |
3 | export class Request {
4 | url: string;
5 | method: MethodType;
6 | headers: { [name: string]: string } = {};
7 | body: any = null;
8 | isExternal: boolean;
9 |
10 | constructor(url: string, method: MethodType, headers: { [name: string]: string; } = {}, body: any = null) {
11 | this.url = url;
12 | this.method = method;
13 | this.headers = headers;
14 | this.body = body;
15 | }
16 |
17 | withMethod(method: MethodType) {
18 | var clone = this.clone();
19 | clone.method = method;
20 | return clone;
21 | }
22 |
23 | withUrl(url: string) {
24 | var clone = this.clone();
25 | clone.url = url;
26 | return clone;
27 | }
28 |
29 | withHeaders(headers: { [name: string]: string; }) {
30 | var clone = this.clone();
31 | clone.headers = { ...clone.headers, ...headers };
32 | return clone;
33 | }
34 |
35 | withHeader(name: string, value: any) {
36 | var clone = this.clone();
37 | clone.headers[name] = value;
38 | return clone;
39 | }
40 |
41 | withBody(body: any = null) {
42 | var clone = this.clone();
43 | clone.body = body;
44 | return clone;
45 | }
46 |
47 | clone() {
48 | var clone = new Request(this.url, this.method, this.headers, this.body);
49 | return clone;
50 | }
51 |
52 | // server-side only, makes no difference on front end
53 | markAsExternal() {
54 | return this;
55 | }
56 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/http/response.ts:
--------------------------------------------------------------------------------
1 | export class Response {
2 | url: string;
3 | status: number;
4 | statusText: string;
5 | headers: { [name: string]: string } = {};
6 | body: any = null;
7 |
8 | constructor(url: string, status: number, statusText: string, headers: { [name: string]: string } = {}, body: any = null) {
9 | this.url = url;
10 | this.status = status;
11 | this.statusText = statusText;
12 | this.headers = headers;
13 | this.body = body;
14 | }
15 |
16 | withUrl(url: any) {
17 | var clone = this.clone();
18 | clone.url = url;
19 | return clone;
20 | }
21 |
22 | withStatus(status: number, statusText: string | null = null) {
23 | var clone = this.clone();
24 | clone.status = status;
25 | if (statusText !== null) {
26 | clone.statusText = statusText;
27 | }
28 | return clone;
29 | }
30 |
31 | withHeaders(headers: { [name: string]: string }) {
32 | var clone = this.clone();
33 | clone.headers = { ...clone.headers, ...headers };
34 | return clone;
35 | }
36 |
37 | withHeader(name: string | number, value: any) {
38 | var clone = this.clone();
39 | clone.headers[name] = value;
40 | return clone;
41 | }
42 |
43 | withBody(body: any = null) {
44 | var clone = this.clone();
45 | clone.body = body;
46 | return clone;
47 | }
48 |
49 | ok() {
50 | return this.status >= 200 && this.status < 300;
51 | }
52 |
53 | clone() {
54 | var clone = new Response(this.url, this.status, this.statusText, this.headers, this.body);
55 | return clone;
56 | }
57 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/http/runRequest.ts:
--------------------------------------------------------------------------------
1 | import { isBlob } from "../helpers/isBlob";
2 | import { MethodType } from "./methodType";
3 | import { Response } from "./response";
4 |
5 | export function runRequest(
6 | callback: (response: Response) => void,
7 | type: MethodType,
8 | url: string,
9 | data?: any,
10 | headers?: { [name: string]: string | string[] }
11 | ) {
12 | const request = new XMLHttpRequest();
13 | request.onreadystatechange = function () {
14 | if (request.readyState === 4) {
15 | const status = request.status;
16 | const contentType = request.getResponseHeader("Content-Type");
17 | const itsJson = contentType && contentType.indexOf('application/json') === 0;
18 | const raw = request.responseText;
19 | let content = raw;
20 | if (itsJson) {
21 | content = JSON.parse(request.responseText);
22 | }
23 | const headers = {};
24 | const headersString = request.getAllResponseHeaders();
25 | if (headersString) {
26 | const headersArray = headersString.trim().split(/[\r\n]+/);
27 | for (let i = 0; i < headersArray.length; i++) {
28 | const line = headersArray[i];
29 | const parts = line.split(": ");
30 | const header = parts.shift();
31 | if (header) {
32 | const value = parts.join(": ");
33 | headers[header] = value;
34 | }
35 | };
36 | }
37 | const response = new Response(url, status, '', headers, content);
38 | callback(response);
39 | }
40 | }
41 | const isJson = data !== null && typeof data === 'object' && !isBlob(data);
42 | request.open(type.toUpperCase(), url, true);
43 | if (isJson) {
44 | request.setRequestHeader('Content-Type', 'application/json');
45 | }
46 | if (headers) {
47 | for (const h in headers) {
48 | if (Array.isArray(headers[h])) {
49 | for (let i = 0; i < headers[h].length; i++) {
50 | request.setRequestHeader(h, headers[h][i]);
51 | }
52 | } else {
53 | request.setRequestHeader(h, headers[h]);
54 | }
55 | }
56 | }
57 | data !== null ?
58 | request.send(isJson ? JSON.stringify(data) : data)
59 | : request.send();
60 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/hydrate/hydrateComment.ts:
--------------------------------------------------------------------------------
1 | import { getAnchor } from "../anchor/getAnchor";
2 | import { HtmlNodeType } from "../node/htmlNodeType";
3 |
4 | export function hydrateComment(target: HtmlNodeType, content: string): Comment {
5 | const anchor = getAnchor(target);
6 | const max = target.childNodes.length;
7 | let end = anchor.current + 3;
8 | end = end > max ? max : end;
9 | const invalid: number[] = [];
10 | for (let i = anchor.current + 1; i < end; i++) {
11 | const potentialNode = target.childNodes[i];
12 | if (
13 | potentialNode.nodeType === 8
14 | ) {
15 | anchor.current = i;
16 | anchor.invalid = anchor.invalid.concat(invalid);
17 | // console.log('Hydrate match', potentialNode);
18 | return potentialNode as Comment;
19 | }
20 | invalid.push(i);
21 | }
22 | anchor.added++;
23 | anchor.invalid = anchor.invalid.concat(invalid);
24 | console.log('Hydrate comment not found', content);
25 | const element = document.createComment(content);
26 | anchor.current = anchor.current + invalid.length + 1;
27 | return max > anchor.current
28 | ? target.insertBefore(element, target.childNodes[anchor.current])
29 | : target.appendChild(element);
30 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/hydrate/hydrateRaw.ts:
--------------------------------------------------------------------------------
1 | import { Anchor } from "../anchor/anchor";
2 | import { HtmlNodeType } from "../node/htmlNodeType";
3 |
4 | export function hydrateRaw(vdom: HTMLElement, anchor: Anchor, target: HtmlNodeType) {
5 | if (vdom.childNodes.length > 0) {
6 | const invalid: number[] = [];
7 | const rawNodes: HTMLElement[] = Array.prototype.slice.call(vdom.childNodes);
8 | for (let rawNodeI = 0; rawNodeI < rawNodes.length; rawNodeI++) {
9 | const rawNode = rawNodes[rawNodeI];
10 | const rawNodeType = rawNode.nodeType;
11 | if (rawNodeType === 3) {
12 | // text
13 | const currentTargetNode = target.childNodes[anchor.current];
14 | if (currentTargetNode && currentTargetNode.nodeType === rawNodeType) {
15 | currentTargetNode.nodeValue = rawNode.nodeValue;
16 | } else {
17 | anchor.added++;
18 | target.childNodes.length > anchor.current && invalid.push(anchor.current);
19 | target.childNodes.length > anchor.current + 1
20 | ? target.insertBefore(rawNode, target.childNodes[anchor.current + 1])
21 | : target.appendChild(rawNode);
22 | anchor.current++;
23 | // insert
24 | // ? target.parentElement!.insertBefore(rawNode, target)
25 | // : target.appendChild(rawNode);
26 | }
27 | } else {
28 | // other
29 | const currentTargetNode = target.childNodes[anchor.current];
30 | if (
31 | !currentTargetNode
32 | || currentTargetNode.nodeType !== rawNodeType
33 | || (rawNodeType === 1 && currentTargetNode.nodeName !== rawNode.nodeName)
34 | ) {
35 | anchor.added++;
36 | target.childNodes.length > anchor.current && invalid.push(anchor.current);
37 | target.childNodes.length > anchor.current + 1
38 | ? target.insertBefore(rawNode, target.childNodes[anchor.current + 1])
39 | : target.appendChild(rawNode);
40 | anchor.current++;
41 | // mismatch by type
42 | // insert
43 | // ? target.parentElement!.insertBefore(rawNode, target)
44 | // : target.appendChild(rawNode);
45 | } else if (rawNodeType === 1) {
46 | if (currentTargetNode.nodeName !== rawNode.nodeName || (currentTargetNode).outerHTML !== rawNode.outerHTML) {
47 | const keepKey = (currentTargetNode).getAttribute('data-keep');
48 | if (!keepKey || keepKey !== rawNode.getAttribute('data-keep')) { // keep server-side version
49 | (currentTargetNode).outerHTML = rawNode.outerHTML;
50 | }
51 | }
52 | }
53 | // matched, continue
54 | }
55 | anchor.current++;
56 | }
57 | if (invalid.length > 0) {
58 | anchor.invalid = anchor.invalid.concat(invalid);
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/hydrate/hydrateTag.ts:
--------------------------------------------------------------------------------
1 | import { getAnchor } from "../anchor/getAnchor";
2 | import { HtmlNodeType } from "../node/htmlNodeType";
3 |
4 | const specialTags = { body: true, head: true, html: true };
5 |
6 | export function hydrateTag(target: HtmlNodeType, tag: string): HtmlNodeType {
7 | const anchor = getAnchor(target);
8 | const max = target.childNodes.length;
9 | let end = anchor.current + 3;
10 | end = end > max ? max : end;
11 | const invalid: number[] = [];
12 | for (let i = anchor.current + 1; i < end; i++) {
13 | const potentialNode = target.childNodes[i];
14 | if (
15 | potentialNode.nodeType === 1
16 | && potentialNode.nodeName.toLowerCase() === tag.toLowerCase()
17 | ) {
18 | anchor.current = i;
19 | anchor.invalid = anchor.invalid.concat(invalid);
20 | // console.log('Hydrate match', potentialNode);
21 | return potentialNode as Node;
22 | }
23 | invalid.push(i);
24 | }
25 | if (tag in specialTags) {
26 | const nodes = document.getElementsByTagName(tag);
27 | if (nodes.length > 0) {
28 | anchor.invalid = [];
29 | return nodes[0];
30 | }
31 | }
32 | anchor.added++;
33 | anchor.invalid = anchor.invalid.concat(invalid);
34 | console.warn('Hydrate not found', tag);
35 | const element = document.createElement(tag);
36 | anchor.current = anchor.current + invalid.length + 1;
37 | return max > anchor.current
38 | ? target.insertBefore(element, target.childNodes[anchor.current])
39 | : target.appendChild(element);
40 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/hydrate/hydrateText.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { TemplateNode } from "../node/templateNode";
3 | import { getAnchor } from "../anchor/getAnchor";
4 | import { renderText } from "../render/renderText";
5 | import { ContextScope } from "../lifecycle/contextScope";
6 | import { HtmlNodeType } from "../node/htmlNodeType";
7 |
8 | export function hydrateText(target: HtmlNodeType, instance: BaseComponent, node: TemplateNode, scope: ContextScope): Text {
9 | const anchor = getAnchor(target);
10 | const max = target.childNodes.length;
11 | let end = anchor.current + 3;
12 | end = end > max ? max : end;
13 | const invalid: number[] = [];
14 | const start = anchor.current > -1 ? anchor.current : anchor.current + 1;
15 | for (let i = start; i < end; i++) {
16 | const potentialNode = target.childNodes[i];
17 | if (
18 | potentialNode.nodeType === 3
19 | ) {
20 | if (i === anchor.current) {
21 | // text after text, no shift
22 | break;
23 | }
24 | anchor.current = i;
25 | anchor.invalid = anchor.invalid.concat(invalid);
26 | renderText(instance, node, potentialNode as Text, scope);
27 | // console.log('Hydrate match', potentialNode);
28 | return potentialNode as Text;
29 | }
30 | i !== anchor.current && invalid.push(i);
31 | }
32 | anchor.added++;
33 | anchor.invalid = anchor.invalid.concat(invalid);
34 | const textNode = document.createTextNode('');
35 | renderText(instance, node, textNode, scope);
36 | anchor.current = anchor.current + invalid.length + 1;
37 | // console.log('Hydrate not found', textNode);
38 | return max > anchor.current
39 | ? target.insertBefore(textNode, target.childNodes[anchor.current])
40 | : target.appendChild(textNode);
41 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/lifecycle/arrayScope.ts:
--------------------------------------------------------------------------------
1 | import { TextAnchor } from "../anchor/textAnchor";
2 | import { ContextScope } from "./contextScope";
3 |
4 | export type ArrayScope = {
5 | data: { [key: string | number]: { key: string | number, value: any, begin: TextAnchor, end: TextAnchor, scope: ContextScope } }
6 | };
7 |
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/lifecycle/contextScope.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { Slots } from "../node/slots";
3 |
4 | export type ContextScope = {
5 | counter: number, // current id counter
6 | id: number, // unique per parent scope
7 | why: ContextReason,
8 | instance: BaseComponent,
9 | lastComponent: { instance: BaseComponent | null },
10 | main?: boolean, // first scope of the instance and should be disposed
11 | arguments: any[], // array (foreach directive) arguments
12 | map: { [key: string]: number }, // array (foreach directive) arguments positions
13 | track: { path: string, id: number }[], // disposable reactivity items
14 | parent?: ContextScope,
15 | children: { [key: string]: ContextScope }, // all nested scopes from directives and components, tree disposal
16 | slots?: Slots,
17 | refs?: { [key: string]: boolean },
18 | keep?: boolean // do not dispose
19 | }
20 |
21 | export type ContextReason = 'if' | 'elseif' | 'else' | 'foreach' | 'forItem' | 'dynamic' | 'slot' | 'component' | string
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/lifecycle/dispose.ts:
--------------------------------------------------------------------------------
1 | import { ContextScope } from "./contextScope";
2 |
3 | export function dispose(scope: ContextScope) {
4 | // dispose reactivity tracker for removed html/template nodes
5 | if (scope.keep) return;
6 | for (let reactivityIndex in scope.track) {
7 | const reactivityItem = scope.track[reactivityIndex];
8 | delete scope.instance.$$r[reactivityItem.path][reactivityItem.id];
9 | }
10 | scope.track = [];
11 | if (scope.children) {
12 | for (let i in scope.children) {
13 | dispose(scope.children[i]);
14 | }
15 | scope.children = {};
16 | }
17 | if (scope.main) {
18 | // dispose instance
19 | for (let i = 0; i < scope.instance.$$p.length; i++) {
20 | const trackGroup = scope.instance.$$p[i];
21 | delete trackGroup[1].$$r[trackGroup[0]];
22 | }
23 | // TODO: call dispose hook
24 | const instance = scope.instance as any;
25 | if (instance.destroy) {
26 | instance.destroy();
27 | }
28 | }
29 | if (scope.parent) {
30 | delete scope.parent.children[scope.id];
31 | delete scope.parent;
32 | }
33 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/lifecycle/iDestroyable.ts:
--------------------------------------------------------------------------------
1 | export interface IDestroyable {
2 | destroy(): void;
3 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/lifecycle/imiddleware.ts:
--------------------------------------------------------------------------------
1 | export type IMiddleware = {
2 | run(context: { next: (allow: boolean) => void }): void;
3 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/lifecycle/propsContext.ts:
--------------------------------------------------------------------------------
1 | import { ContextScope } from "./contextScope"
2 | import { TemplateNode } from "../node/templateNode"
3 |
4 | export type PropsContext = {
5 | attributes: TemplateNode[]
6 | scope: ContextScope
7 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/lifecycle/scopeState.ts:
--------------------------------------------------------------------------------
1 | type ScopeState = { http: { [key: string]: any }, state: { [component: string]: { [prop: string]: any } } };
2 |
3 | export function getScopeState(): ScopeState {
4 | const scopedResponseData: undefined | ScopeState = (window).viewiScopeState;
5 | return scopedResponseData ?? { http: {}, state: {} };
6 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/node/htmlNodeType.ts:
--------------------------------------------------------------------------------
1 | export type HtmlNodeType = Node & { isSvg?: boolean }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/node/inputType.ts:
--------------------------------------------------------------------------------
1 | export type InputType = 'text' | 'checkbox' | 'radio' | 'select'
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/node/nodeType.ts:
--------------------------------------------------------------------------------
1 | export type NodeType = 'tag' | 'attr' | 'value' | 'component' | 'text' | 'comment' | 'root' | 'doctype' | undefined;
2 | export type NodeTypePacked = 't' | 'a' | 'v' | 'x' | 'm' | 'c' | 'r' | 'd' | undefined;
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/node/slots.ts:
--------------------------------------------------------------------------------
1 | import { ContextScope } from "../lifecycle/contextScope";
2 | import { TemplateNode } from "./templateNode";
3 |
4 | export type Slots = { [key: string]: { node: TemplateNode, scope: ContextScope } }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/node/templateNode.ts:
--------------------------------------------------------------------------------
1 | import { NodeType, NodeTypePacked } from "./nodeType"
2 |
3 | export type TemplateNode = {
4 | t: NodeTypePacked,
5 | type: NodeType,
6 | c?: string,
7 | content?: string,
8 | code?: number,
9 | subs?: string[],
10 | e?: boolean,
11 | expression?: boolean,
12 | raw?: boolean,
13 | h?: TemplateNode[],
14 | children?: TemplateNode[],
15 | a?: TemplateNode[],
16 | attributes?: TemplateNode[],
17 | i?: TemplateNode[],
18 | slots?: TemplateNode[],
19 | directives?: TemplateNode[],
20 | unpacked?: boolean,
21 | dynamic?: TemplateNode,
22 | forData?: number,
23 | forItem?: string,
24 | forKey?: string,
25 | forKeyAuto?: boolean,
26 | func: Function,
27 | first?: boolean
28 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/node/unpack.ts:
--------------------------------------------------------------------------------
1 | import { NodeType } from "./nodeType";
2 | import { TemplateNode } from "./templateNode";
3 |
4 | export function unpack(item: TemplateNode) {
5 | let nodeType: NodeType = 'value';
6 | switch (item.t) {
7 | case 't': {
8 | nodeType = 'tag';
9 | break;
10 | }
11 | case 'a': {
12 | nodeType = 'attr';
13 | break;
14 | }
15 | case undefined:
16 | case 'v': {
17 | nodeType = 'value';
18 | break;
19 | }
20 | case 'c': {
21 | nodeType = 'component';
22 | break;
23 | }
24 | case 'x': {
25 | nodeType = 'text';
26 | break;
27 | }
28 | case 'm': {
29 | nodeType = 'comment';
30 | break;
31 | }
32 | case 'd': {
33 | nodeType = 'doctype';
34 | break;
35 | }
36 | case 'r': {
37 | nodeType = 'root';
38 | // doctype node 10
39 | if (item.h && item.h[0].t === "x" && item.h[0].c?.substring(0, 9) === ' implements IRenderable, IDestroyable {
13 | to?: string;
14 | name?: string;
15 | _name: string = 'Portal';
16 | anchorNode?: TextAnchor;
17 |
18 | destroy(): void {
19 | if (this.to && this.anchorNode && this.anchorNode.previousSibling) {
20 | while (this.anchorNode.previousSibling._anchor !== this.anchorNode._anchor) {
21 | this.anchorNode.previousSibling!.remove();
22 | }
23 | this.anchorNode.previousSibling!.remove();
24 | this.anchorNode.remove();
25 | }
26 | }
27 |
28 | render(target: HtmlNodeType, name: string, scope: ContextScope, props?: PropsContext, hydrate = false, insert = false, params: { [key: string]: any } = {}): void {
29 | if (this.name) {
30 | const idEnd = 'portal_' + this.name + '_end';
31 | if (hydrate) {
32 | const portalEndMark = document.getElementById(idEnd);
33 | if (portalEndMark) {
34 | const portalPositionIndex = Array.prototype.indexOf.call(target.childNodes, portalEndMark);
35 | if (portalPositionIndex > 0) {
36 | const anchor = getAnchor(target);
37 | if (!(this.name in portals)) {
38 | portals[this.name] = {};
39 | }
40 | portals[this.name].current = anchor.current + 1;
41 | anchor.current = portalPositionIndex;
42 | if (portals[this.name].queue) {
43 | const queue = portals[this.name].queue;
44 | for (let i = 0; i < queue.length; i++) {
45 | queue[i][0].apply(null, queue[i][1]);
46 | }
47 | delete portals[this.name].queue;
48 | }
49 | }
50 | }
51 | } else {
52 | const idBegin = 'portal_' + this.name;
53 | const portalBeginElement = document.createElement('i');
54 | const portalEndElement = document.createElement('i');
55 | portalBeginElement.setAttribute('id', idBegin);
56 | portalEndElement.setAttribute('id', idEnd);
57 | const style = 'display: none !important;';
58 | portalBeginElement.setAttribute('style', style);
59 | portalEndElement.setAttribute('style', style);
60 | insert
61 | ? target.parentElement!.insertBefore(portalBeginElement, target)
62 | : target.appendChild(portalBeginElement);
63 | insert
64 | ? target.parentElement!.insertBefore(portalEndElement, target)
65 | : target.appendChild(portalEndElement);
66 | }
67 | } else if (this.to) {
68 | if (hydrate) {
69 | if (this.to in portals && portals[this.to].current) {
70 | renderPortal(this, scope, hydrate, insert);
71 | } else {
72 | const delayedRender = [renderPortal, [this, scope, hydrate, insert]];
73 | if (this.to in portals) {
74 | portals[this.to].queue.push(delayedRender);
75 | } else {
76 | portals[this.to] = {
77 | queue: [delayedRender]
78 | };
79 | }
80 | }
81 | } else {
82 | renderPortal(this, scope, false, true);
83 | }
84 | } else {
85 | throw new Error("Portal component should have either 'name' or 'to' attribute.");
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/portal/portals.ts:
--------------------------------------------------------------------------------
1 | export const portals = {};
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/portal/renderPortal.ts:
--------------------------------------------------------------------------------
1 | import { createAnchorNode } from "../anchor/createAnchorNode";
2 | import { getAnchor } from "../anchor/getAnchor";
3 | import { globalScope } from "../di/globalScope";
4 | import { ContextScope } from "../lifecycle/contextScope";
5 | import { unpack } from "../node/unpack";
6 | import { render } from "../render/render";
7 | import { Portal } from "./portal";
8 | import { portals } from "./portals";
9 |
10 | export function renderPortal(portal: Portal, scope: ContextScope, hydrate = false, insert = false) {
11 | const portalEndMark = document.getElementById('portal_' + portal.to! + '_end');
12 | if (portalEndMark) {
13 | const portalAnchorCurrent = portals[portal.to!].current;
14 | const renderTarget = insert ? portalEndMark : portalEndMark.parentElement!;
15 | const anchor = hydrate ? getAnchor(renderTarget) : undefined;
16 | const anchorCurrent = hydrate ? anchor!.current : 0;
17 | const portalPositionIndexBefore = Array.prototype.indexOf.call(renderTarget.childNodes, portalEndMark);
18 | hydrate && (anchor!.current = portalAnchorCurrent);
19 | // render
20 | let slotName: string = 'default';
21 | const anchorSlotBegin = createAnchorNode(renderTarget, insert, anchor); // begin slot
22 | if (slotName in scope.slots!) { // slot from parent
23 | const slot = scope.slots![slotName];
24 | if (!slot.node.unpacked) {
25 | unpack(slot.node);
26 | slot.node.unpacked = true;
27 | }
28 | render(renderTarget, slot.scope.instance, slot.node.children!, slot.scope, undefined, hydrate, insert);
29 | }
30 | const anchorSlotNode = createAnchorNode(renderTarget, insert, anchor, anchorSlotBegin!._anchor); // end slot
31 | if (scope.instance._name in globalScope.iteration) {
32 | globalScope.iteration[scope.instance._name].slots[slotName] = anchorSlotNode;
33 | }
34 | portal.anchorNode = anchorSlotNode;
35 | // restore anchor position
36 | if (hydrate) {
37 | portals[portal.to!].current = anchor!.current;
38 | const portalPositionIndexAfter = Array.prototype.indexOf.call(renderTarget.childNodes, portalEndMark);
39 | anchor!.current = anchorCurrent + portalPositionIndexAfter - portalPositionIndexBefore;
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/handlers/getComponentModelHandler.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../../component/baseComponent";
2 |
3 | export function getComponentModelHandler(instance: BaseComponent, setter: (instance: BaseComponent, value: any) => void) {
4 | return function (event: any) {
5 | setter(instance, event);
6 | }
7 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/handlers/getModelHandler.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../../component/baseComponent";
2 | import { ModelHandler } from "./modelHandler";
3 | import { HTMLModelInputElement } from "./updateModelValue";
4 |
5 | export function getModelHandler(
6 | instance: BaseComponent,
7 | options: ModelHandler
8 | ): EventListener {
9 | return function (event: Event & {
10 | target: HTMLModelInputElement
11 | }) {
12 | if (options.inputType === "checkbox") {
13 | const currentValue = options.getter(instance);
14 | const inputValue = event.target.value;
15 | if (Array.isArray(currentValue)) {
16 | const newValue = currentValue.slice();
17 | const valuePosition = newValue.indexOf(inputValue);
18 | if (valuePosition === -1) {
19 | if (event.target.checked) {
20 | newValue.push(inputValue);
21 | }
22 | } else {
23 | if (!event.target.checked) {
24 | newValue.splice(valuePosition, 1);
25 | }
26 | }
27 | options.setter(instance, newValue);
28 | } else {
29 | options.setter(instance, event.target.checked);
30 | }
31 | } else if (options.inputType === "radio") {
32 | const inputValue = event.target.value;
33 | options.setter(instance, inputValue);
34 | } else if (options.isMultiple || event.target.multiple) {
35 | const inputOptions = event.target.options;
36 | const newValue: string[] = [];
37 | for (let i = 0; i < inputOptions.length; i++) {
38 | const currentOption = inputOptions[i];
39 | if (currentOption.selected) {
40 | newValue.push(currentOption.value);
41 | }
42 | }
43 | options.setter(instance, newValue);
44 | } else {
45 | options.setter(instance, event.target.value);
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/handlers/modelHandler.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../../component/baseComponent"
2 | import { InputType } from "../../node/inputType"
3 |
4 | export type ModelHandler = {
5 | getter: (instance: BaseComponent) => any,
6 | setter: (instance: BaseComponent, value: any) => void,
7 | inputType: InputType,
8 | isMultiple: boolean
9 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/handlers/updateComment.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../../component/baseComponent";
2 | import { TemplateNode } from "../../node/templateNode";
3 |
4 | export function updateComment(instance: BaseComponent, node: TemplateNode, commentNode: Comment) {
5 | const content = node.expression
6 | ? instance.$$t[node.code as number](instance)
7 | : (node.content ?? '');
8 | commentNode.nodeValue !== content && (commentNode.nodeValue = content);
9 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/handlers/updateComponentModel.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../../component/baseComponent";
2 |
3 | export function updateComponentModel(
4 | instance: BaseComponent,
5 | attrName: string,
6 | getter: (instance: BaseComponent) => any,
7 | parentInstance: BaseComponent
8 | ) {
9 | instance[attrName] = getter(parentInstance);
10 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/handlers/updateModelValue.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../../component/baseComponent";
2 | import { ModelHandler } from "./modelHandler";
3 |
4 | export type HTMLModelInputElement = HTMLInputElement & HTMLSelectElement;
5 |
6 | export function updateModelValue(
7 | target: HTMLModelInputElement,
8 | instance: BaseComponent,
9 | options: ModelHandler
10 | ): void {
11 | if (options.inputType === "checkbox") {
12 | const currentValue = options.getter(instance);
13 | if (Array.isArray(currentValue)) {
14 | const inputValue = target.value;
15 | const valuePosition = currentValue.indexOf(inputValue);
16 | if (valuePosition === -1) {
17 | target.removeAttribute('checked');
18 | target.checked = false;
19 | } else {
20 | target.setAttribute('checked', 'checked');
21 | target.checked = true;
22 | }
23 | } else {
24 | if (currentValue) {
25 | target.setAttribute('checked', 'checked');
26 | target.checked = true;
27 | } else {
28 | target.removeAttribute('checked');
29 | target.checked = false;
30 | }
31 | }
32 | } else if (options.inputType === "radio") {
33 | const currentValue = options.getter(instance);
34 | const inputValue = target.value;
35 | if (currentValue === inputValue) {
36 | target.setAttribute('checked', 'checked');
37 | target.checked = true;
38 | } else {
39 | target.removeAttribute('checked');
40 | target.checked = false;
41 | }
42 | } else if (options.isMultiple || target.multiple) {
43 | const inputOptions = target.options;
44 | const currentValue = options.getter(instance);
45 | for (let i = 0; i < inputOptions.length; i++) {
46 | const currentOption = inputOptions[i];
47 | const index = currentValue.indexOf(currentOption.value);
48 | if (index === -1) {
49 | currentOption.selected = false;
50 | } else {
51 | currentOption.selected = true;
52 | }
53 | }
54 | } else {
55 | target.value = options.getter(instance);
56 | }
57 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/handlers/updateProp.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../../component/baseComponent";
2 | import { TemplateNode } from "../../node/templateNode";
3 | import { PropsContext } from "../../lifecycle/propsContext";
4 |
5 | export function updateProp(instance: BaseComponent, attribute: TemplateNode, props: PropsContext) {
6 | const parentInstance = props.scope.instance;
7 | const attrName = attribute.expression
8 | ? parentInstance.$$t[attribute.code!](parentInstance) // TODO: arguments
9 | : (attribute.content ?? '');
10 | if (attrName[0] === '(') {
11 | // TODO: event
12 | } else {
13 | let valueContent: any = null;
14 | let valueSubs = []; // TODO: on backend, pass attribute value subs in attribute
15 | if (attribute.children) {
16 | for (let av = 0; av < attribute.children.length; av++) {
17 | const attributeValue = attribute.children[av];
18 | let callArguments = [parentInstance];
19 | if (props.scope.arguments) {
20 | callArguments = callArguments.concat(props.scope.arguments);
21 | }
22 | const childContent = attributeValue.expression
23 | ? parentInstance.$$t[attributeValue.code as number].apply(null, callArguments)
24 | : (attributeValue.content ?? '');
25 | valueContent = av === 0 ? childContent : valueContent + (childContent ?? '');
26 | if (attributeValue.subs) {
27 | valueSubs = valueSubs.concat(attributeValue.subs as never[]);
28 | }
29 | }
30 | }
31 | if (attrName === '_props' && valueContent) {
32 | for (let propName in valueContent) {
33 | instance[propName] = valueContent[propName];
34 | instance._props[propName] = valueContent[propName];
35 | }
36 | } else {
37 | instance[attrName] = valueContent;
38 | instance._props[attrName] = valueContent;
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/makeProxy.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { ReserverProps } from "../component/reserverProps";
3 |
4 | let reactiveId = 0;
5 |
6 | let queue = {};
7 |
8 | let timeoutId: number = 0;
9 |
10 | function executeQueue() {
11 | timeoutId = 0;
12 | const currentQueue = queue;
13 | queue = {};
14 | for (let uid in currentQueue) {
15 | const callbackFunc = currentQueue[uid];
16 | try {
17 | callbackFunc[0].apply(null, callbackFunc[1]);
18 | } catch (err) {
19 | console.error(err);
20 | }
21 | }
22 | }
23 |
24 | function schedule(path: string, i: string, callbackFunc: any) {
25 | queue[path + '-' + i] = callbackFunc;
26 | if (timeoutId === 0) {
27 | timeoutId = setTimeout(executeQueue, 0);
28 | }
29 | }
30 |
31 | export type ReactiveProxy = object & { $: ReactiveProxy, $$r?: { [key: string]: [path: string, instance: BaseComponent] } };
32 |
33 | export function activateTarget(component: T & BaseComponent, mainPath: string, prop: string, target: any) {
34 | let val = target[prop];
35 | if (!Object.getOwnPropertyDescriptor(target, prop)?.set) {
36 | Object.defineProperty(target, prop, {
37 | enumerable: true,
38 | configurable: true,
39 | get: function () {
40 | return val;
41 | },
42 | set: function (value) {
43 | const react = val !== value;
44 | val = value;
45 | deepProxy(mainPath, component, val);
46 | if (react) {
47 | for (let id in target.$$r) {
48 | const path = target.$$r[id][0];
49 | const component = target.$$r[id][1];
50 | // const propertyPath = path + '.' + prop;
51 | // if (propertyPath in component.$$r) {
52 | // for (let i in component.$$r[propertyPath]) {
53 | // const callbackFunc = component.$$r[propertyPath][i];
54 | // // TODO: schedule queue and react only once
55 | // callbackFunc[0].apply(null, callbackFunc[1]);
56 | // }
57 | // }
58 | // All root path dependencies should trigger updates, no need for sub path updates
59 | if (path in component.$$r) {
60 | for (let i in component.$$r[path]) {
61 | const callbackFunc = component.$$r[path][i];
62 | schedule(path, i, callbackFunc);
63 | }
64 | }
65 | }
66 | }
67 | }
68 | });
69 | deepProxy(mainPath, component, val);
70 | }
71 | }
72 |
73 |
74 | function deepProxy(prop: string, component: T & BaseComponent, targetObject: ReactiveProxy) {
75 | if (!(prop in ReserverProps)) {
76 | if (Array.isArray(targetObject)) {
77 | // TODO:
78 | }
79 | else if (targetObject !== null && typeof targetObject === 'object' && typeof targetObject !== 'function' && !(targetObject instanceof EventTarget)) {
80 | if (!('$$r' in targetObject)) {
81 | Object.defineProperty(targetObject, "$$r", {
82 | enumerable: false,
83 | writable: true,
84 | value: {}
85 | });
86 | }
87 | let keys = Object.keys(targetObject);
88 | for (let i = 0; i < keys.length; i++) {
89 | const valueProp = keys[i];
90 | if (!(valueProp in ReserverProps)) {
91 | activateTarget(component, prop, valueProp, targetObject);
92 | }
93 | }
94 | const trackerId = ++reactiveId + '';
95 | targetObject.$$r![trackerId] = [prop, component];
96 | component.$$p.push([trackerId, targetObject]);
97 | }
98 | }
99 | }
100 |
101 | export function defineReactive(component: T & BaseComponent, prop: string) {
102 | let val = component[prop];
103 | deepProxy(prop, component, val);
104 | Object.defineProperty(component, prop, {
105 | enumerable: true,
106 | configurable: true,
107 | get: function () {
108 | return val;
109 | },
110 | set: function (value) {
111 | const react = val !== value;
112 | val = value;
113 | deepProxy(prop, component, val);
114 | if (react && (prop in component.$$r)) {
115 | for (let i in component.$$r[prop]) {
116 | const callbackFunc = component.$$r[prop][i];
117 | schedule(prop, i, callbackFunc);
118 | }
119 | }
120 | }
121 | });
122 | }
123 |
124 | export function makeProxy(component: T & BaseComponent): T {
125 | let keys = Object.keys(component);
126 | for (let i = 0; i < keys.length; i++) {
127 | const prop = keys[i];
128 | if (!(prop in ReserverProps)) {
129 | defineReactive(component, prop);
130 | }
131 | }
132 | return component;
133 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/makeProxyOrigin.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { ReserverProps } from "../component/reserverProps";
3 |
4 | let reactiveId = 0;
5 |
6 | export type ReactiveProxy = object & { $: ReactiveProxy, $$r: { [key: string]: [path: string, instance: BaseComponent] } };
7 |
8 | export function makeReactive(componentProperty: ReactiveProxy, component: BaseComponent, path: string): ReactiveProxy {
9 | const targetObject = componentProperty.$ ?? componentProperty;
10 | if (!targetObject.$) {
11 | Object.defineProperty(targetObject, "$", {
12 | enumerable: false,
13 | writable: true,
14 | value: targetObject
15 | });
16 | Object.defineProperty(targetObject, "$$r", {
17 | enumerable: false,
18 | writable: true,
19 | value: {}
20 | });
21 | }
22 | const proxy = new Proxy(targetObject, {
23 | set(obj, prop: string, value: any) {
24 | const react = obj[prop] !== value;
25 | const ret = Reflect.set(obj, prop, value);
26 | if (react) {
27 | for (let id in obj.$$r) {
28 | const path = obj.$$r[id][0];
29 | const component = obj.$$r[id][1];
30 | // const propertyPath = path + '.' + prop;
31 | // if (propertyPath in component.$$r) {
32 | // for (let i in component.$$r[propertyPath]) {
33 | // const callbackFunc = component.$$r[propertyPath][i];
34 | // // TODO: schedule queue and react only once
35 | // callbackFunc[0].apply(null, callbackFunc[1]);
36 | // }
37 | // }
38 | // All root path dependencies should trigger updates, no need for sub path updates
39 | if (path in component.$$r) {
40 | for (let i in component.$$r[path]) {
41 | const callbackFunc = component.$$r[path][i];
42 | // TODO: schedule queue and react only once
43 | callbackFunc[0].apply(null, callbackFunc[1]);
44 | }
45 | }
46 | }
47 | }
48 | return ret;
49 | }
50 | });
51 | return proxy;
52 | }
53 |
54 | function deepProxy(prop: string, component: T & BaseComponent, value: any) {
55 | if (!(prop in ReserverProps) && value !== null && typeof value === 'object' && !Array.isArray(value)) {
56 | const activated = makeReactive(value, component, prop);
57 | component[prop] = activated;
58 | const trackerId = ++reactiveId + '';
59 | activated.$$r[trackerId] = [prop, component];
60 | component.$$p.push([trackerId, activated]);
61 | }
62 | }
63 |
64 | export function makeProxy(component: T & BaseComponent): T {
65 | let keys = Object.keys(component);
66 | for (let i = 0; i < keys.length; i++) {
67 | const key = keys[i];
68 | const val = component[key];
69 | deepProxy(key, component, val);
70 | }
71 | const proxy = new Proxy(component, {
72 | set(obj, prop: string, value) {
73 | const react = obj[prop] !== value;
74 | const ret = Reflect.set(obj, prop, value);
75 | deepProxy(prop, component, value);
76 | if (react && (prop in obj.$$r)) {
77 | for (let i in obj.$$r[prop]) {
78 | const callbackFunc = obj.$$r[prop][i];
79 | // TODO: schedule queue and react only once
80 | callbackFunc[0].apply(null, callbackFunc[1]);
81 | }
82 | }
83 | return ret;
84 | }
85 | });
86 | component.$ = component;
87 | return proxy;
88 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/reactivity/track.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { ContextScope } from "../lifecycle/contextScope";
3 |
4 | let trackingId = 0;
5 | export function nextTrackingId() {
6 | return ++trackingId;
7 | }
8 |
9 | export function track(instance: BaseComponent, trackingPath: string, scope: ContextScope, action: [Function, any[]]) {
10 | if (!instance.$$r[trackingPath]) {
11 | instance.$$r[trackingPath] = {};
12 | }
13 | const trackId = ++trackingId;
14 | scope.track.push({ id: trackId, path: trackingPath });
15 | instance.$$r[trackingPath][trackId] = action;
16 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/render/iRenderable.ts:
--------------------------------------------------------------------------------
1 | import { ContextScope } from "../lifecycle/contextScope";
2 | import { PropsContext } from "../lifecycle/propsContext";
3 | import { HtmlNodeType } from "../node/htmlNodeType";
4 |
5 | export interface IRenderable {
6 | render(target: HtmlNodeType, name: string, scope: ContextScope, props: PropsContext | undefined, hydrate: boolean, insert: boolean, params: { [key: string]: any }): void;
7 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/render/renderApp.ts:
--------------------------------------------------------------------------------
1 | import { resources } from "../../../app/main/resources";
2 | import { anchors } from "../anchor/anchors";
3 | import { componentsMeta } from "../component/componentsMeta";
4 | import { delay } from "../di/delay";
5 | import { globalScope } from "../di/globalScope";
6 | import { resolve } from "../di/resolve";
7 | import { injectScript } from "../http/injectScript";
8 | import { dispose } from "../lifecycle/dispose";
9 | import { IMiddleware } from "../lifecycle/imiddleware";
10 | import { HtmlNodeType } from "../node/htmlNodeType";
11 | import { renderComponent } from "./renderComponent";
12 |
13 | const lazyRecords = {};
14 |
15 | export function renderApp(
16 | name: string,
17 | params: { [key: string]: any },
18 | target?: HtmlNodeType,
19 | onAccept?: { func: (href: string, forward: boolean) => void, href: string, forward: boolean },
20 | skipMiddleware?: boolean
21 | ) {
22 | globalScope.cancel = false;
23 | // console.time('renderApp');
24 | if (!(name in componentsMeta.list)) {
25 | throw new Error(`Component ${name} not found.`);
26 | }
27 | const info = componentsMeta.list[name];
28 | if (info.lazy && !(info.lazy in lazyRecords)) {
29 | const baseName = 'viewi' + (resources.name === 'default' ? '' : '.' + resources.name);
30 | const scriptUrl = resources.publicPath + baseName + '.' + info.lazy + (resources.minify ? '.min' : '') + '.js'
31 | + (resources.appendVersion ? '?' + resources.build : '');
32 | injectScript(scriptUrl);
33 | delay.postpone(info.lazy, function () {
34 | lazyRecords[info.lazy!] = true;
35 | renderApp(name, params, target, onAccept, skipMiddleware);
36 | });
37 | return;
38 | }
39 | const hydrate = globalScope.hydrate;
40 | const lastScope = globalScope.rootScope;
41 | if (onAccept) {
42 | if (lastScope && info.parent !== globalScope.layout) {
43 | // new html root, can't render, request from server
44 | location.href = onAccept.href;
45 | return;
46 | }
47 | }
48 |
49 | if (info.middleware && !skipMiddleware) {
50 | const total = info.middleware.length;
51 | let globalAllow = true;
52 | let current = -1;
53 | const context = {
54 | next: function (allow: boolean = true) {
55 | globalAllow = allow;
56 | current++;
57 | if (globalAllow && current < total) {
58 | // run next middleware
59 | const middleware: IMiddleware = resolve(info.middleware![current]);
60 | middleware.run(context);
61 | } else {
62 | // render app
63 | if (globalAllow) {
64 | renderApp(name, params, target, onAccept, true);
65 | } else {
66 | // keep!
67 | }
68 | }
69 | }
70 | };
71 | context.next(true);
72 | return;
73 | }
74 | if (onAccept) {
75 | onAccept.func(onAccept.href, onAccept.forward);
76 | }
77 | globalScope.layout = info.parent!;
78 | globalScope.lastIteration = globalScope.iteration;
79 | globalScope.iteration = {};
80 | globalScope.scopedContainer = {};
81 | globalScope.located = {};
82 | globalScope.rootScope = renderComponent(target ?? document, name, undefined, {}, hydrate, false, params);
83 | globalScope.hydrate = false; // TODO: scope managment function
84 | for (let name in globalScope.lastIteration) {
85 | if (!(name in globalScope.iteration)) {
86 | globalScope.lastIteration[name].scope.keep = false;
87 | }
88 | }
89 | lastScope && dispose(lastScope);
90 | // console.log(anchors);
91 | // return;
92 |
93 | // Clean up unhydrated content
94 | if (hydrate) {
95 | for (let a in anchors) {
96 | const anchor = anchors[a];
97 | // clean up what's left
98 | if (anchor.target.nodeName !== 'HEAD' && anchor.target.nodeName !== 'BODY') {
99 | for (let i = anchor.target.childNodes.length - 1; i >= anchor.current + 1; i--) {
100 | anchor.target.childNodes[i].remove();
101 | }
102 | }
103 | // clean up not matched
104 | for (let i = anchor.invalid.length - 1; i >= 0; i--) {
105 | anchor.target.childNodes[anchor.invalid[i]].remove();
106 | }
107 | }
108 | }
109 | // console.timeEnd('renderApp');
110 | // console.timeLog('renderApp');
111 | // console.timeEnd('renderApp');
112 | // console.log(globalScope);
113 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/render/renderAttributeValue.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { TemplateNode } from "../node/templateNode";
3 | import { componentsMeta } from "../component/componentsMeta";
4 | import { ContextScope } from "../lifecycle/contextScope";
5 | import { HtmlNodeType } from "../node/htmlNodeType";
6 | import { xLinkNs } from "../helpers/isSvg";
7 |
8 | export function renderAttributeValue(
9 | instance: BaseComponent,
10 | attribute: TemplateNode,
11 | element: HTMLElement & HtmlNodeType,
12 | attrName: string,
13 | scope: ContextScope
14 | ) {
15 | let valueContent: string | boolean | null = null;
16 | if (attribute.children) {
17 | valueContent = '';
18 | for (let av = 0; av < attribute.children.length; av++) {
19 | const attributeValue = attribute.children[av];
20 | let callArguments = [instance];
21 | if (scope.arguments) {
22 | callArguments = callArguments.concat(scope.arguments);
23 | }
24 | const childContent = attributeValue.expression
25 | ? instance.$$t[attributeValue.code as number].apply(null, callArguments)
26 | : (attributeValue.content ?? '');
27 | valueContent = av === 0 ? childContent : valueContent + (childContent ?? '');
28 | }
29 | }
30 | if (attrName.toLowerCase() in componentsMeta.booleanAttributes) {
31 | if (valueContent === true || valueContent === null) {
32 | attrName !== element.getAttribute(attrName) && element.setAttribute(attrName, attrName);
33 | } else {
34 | element.removeAttribute(attrName);
35 | }
36 | } else {
37 | if (element.isSvg && attrName.startsWith('xlink:')) {
38 | if (valueContent !== null) {
39 | valueContent !== element.getAttribute(attrName) && element.setAttributeNS(xLinkNs, attrName, valueContent);
40 | } else {
41 | element.removeAttributeNS(xLinkNs, attrName.slice(6, attrName.length));
42 | }
43 | } else {
44 | if (valueContent !== null) {
45 | valueContent !== element.getAttribute(attrName) && element.setAttribute(attrName, valueContent);
46 | } else {
47 | element.removeAttribute(attrName);
48 | }
49 | }
50 | }
51 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/render/renderComponent.ts:
--------------------------------------------------------------------------------
1 | import { components } from "../../../app/main/components";
2 | import { BaseComponent } from "../component/baseComponent";
3 | import { componentsMeta } from "../component/componentsMeta";
4 | import { getComponentModelHandler } from "../reactivity/handlers/getComponentModelHandler";
5 | import { makeProxy } from "../reactivity/makeProxy";
6 | import { PropsContext } from "../lifecycle/propsContext";
7 | import { render } from "./render";
8 | import { resolve } from "../di/resolve";
9 | import { Slots } from "../node/slots";
10 | import { track } from "../reactivity/track";
11 | import { unpack } from "../node/unpack";
12 | import { updateComponentModel } from "../reactivity/handlers/updateComponentModel";
13 | import { updateProp } from "../reactivity/handlers/updateProp";
14 | import { ContextScope } from "../lifecycle/contextScope";
15 | import { globalScope } from "../di/globalScope";
16 | import { HtmlNodeType } from "../node/htmlNodeType";
17 | import { IRenderable } from "./iRenderable";
18 |
19 | export function renderComponent(target: HtmlNodeType, name: string, props?: PropsContext, slots?: Slots, hydrate = false, insert = false, params: { [key: string]: any } = {}): ContextScope {
20 | if (!(name in componentsMeta.list)) {
21 | throw new Error(`Component ${name} not found.`);
22 | }
23 | if (!(name in components)) {
24 | throw new Error(`Component ${name} not found.`);
25 | }
26 | const info = componentsMeta.list[name];
27 | const root = info.nodes;
28 | // 'Reuse' is the concept of reusing same layout(s) to avoid rerendering the whole page and side-effects, including visual.
29 | // Top level component tag should be reused along with their rendered content, except slots
30 | // Duplicates are not allowed, otherwise the architecture is wrong and you must reconsider it
31 | const lastIteration = globalScope.lastIteration;
32 | const reuse = name in lastIteration;
33 | if (reuse) {
34 | // clean up previous page slots
35 | const slotHolders = lastIteration[name].slots;
36 | for (let slotName in slotHolders) {
37 | const anchorNode = slotHolders[slotName];
38 | while (anchorNode.previousSibling._anchor !== anchorNode._anchor) {
39 | anchorNode.previousSibling!.remove();
40 | }
41 | }
42 | lastIteration[name].scope.keep = true;
43 | }
44 | const instance: BaseComponent & IRenderable = reuse ? lastIteration[name].instance : makeProxy(resolve(name, params, false, props?.scope.lastComponent.instance || props?.scope.instance || null));
45 | // console.log(name, instance._parent?._name, latestComponent?._name);
46 | if (!reuse) {
47 | if (info.hooks && info.hooks.init) {
48 | (instance as any).init();
49 | }
50 | }
51 | const inlineExpressions = name + '_x';
52 | if (!reuse && inlineExpressions in components) {
53 | instance.$$t = components[inlineExpressions];
54 | }
55 | const scopeId = props ? ++props.scope.counter : 0;
56 | // TODO: on reuse - attach scope to a new parent
57 | const scope: ContextScope = reuse ? lastIteration[name].scope : {
58 | id: scopeId,
59 | why: name,
60 | arguments: [], // props ? [...props.scope.arguments] : [],
61 | instance: instance,
62 | main: true,
63 | map: props ? { ...props.scope.map } : {},
64 | track: [],
65 | children: {},
66 | lastComponent: { instance },//props ? props.scope.lastComponent : null,
67 | counter: 0,
68 | parent: props ? props.scope : undefined,
69 | slots: slots
70 | };
71 |
72 | props && (props.scope.children[scopeId] = scope);
73 | if (info.refs) {
74 | scope.refs = info.refs;
75 | }
76 |
77 | // set props
78 | if (props && props.attributes) {
79 | const parentInstance = props.scope.instance;
80 | for (let a in props.attributes) {
81 | let callArguments = [parentInstance];
82 | if (props.scope.arguments) {
83 | callArguments = callArguments.concat(props.scope.arguments);
84 | }
85 | const attribute = props.attributes[a];
86 | const attrName = attribute.expression
87 | ? parentInstance.$$t[attribute.code!].apply(null, callArguments)
88 | : (attribute.content ?? '');
89 | if (attrName[0] === '(') {
90 | const eventName = attrName.substring(1, attrName.length - 1);
91 | if (attribute.children) {
92 | const eventHandler =
93 | parentInstance.$$t[
94 | attribute.dynamic
95 | ? attribute.dynamic.code!
96 | : attribute.children[0].code!
97 | ].apply(null, callArguments) as EventListener;
98 | instance.$_callbacks[eventName] = eventHandler;
99 | // console.log('Event', attribute, eventName, eventHandler);
100 | }
101 | } else if (attrName[0] === '#') {
102 | const refName = attrName.substring(1, attrName.length);
103 | parentInstance._refs[refName] = instance;
104 | if (refName in parentInstance) {
105 | parentInstance[refName] = instance;
106 | }
107 | } else {
108 | const isModel = attrName === 'model';
109 | let valueContent: any = null;
110 | let valueSubs = []; // TODO: on backend, pass attribute value subs in attribute
111 | if (isModel) {
112 | const attributeValue = attribute.children![0];
113 | const getterSetter = parentInstance.$$t[attributeValue.code as number].apply(null, callArguments);
114 | valueContent = getterSetter[0](parentInstance);
115 | instance.$_callbacks[attrName] = getComponentModelHandler(parentInstance, getterSetter[1]);
116 | for (let subI in attributeValue.subs!) {
117 | const trackingPath = attributeValue.subs[subI];
118 | track(parentInstance, trackingPath, props.scope, [updateComponentModel, [instance, attrName, getterSetter[0], parentInstance]]);
119 | }
120 | } else {
121 | if (attribute.children) {
122 | for (let av = 0; av < attribute.children.length; av++) {
123 | const attributeValue = attribute.children[av];
124 | let callArguments = [parentInstance];
125 | if (props.scope.arguments) {
126 | callArguments = callArguments.concat(props.scope.arguments);
127 | }
128 | const childContent = attributeValue.expression
129 | ? parentInstance.$$t[attributeValue.code as number].apply(null, callArguments)
130 | : (attributeValue.content ?? '');
131 | valueContent = av === 0 ? childContent : valueContent + (childContent ?? '');
132 | if (attributeValue.subs) {
133 | valueSubs = valueSubs.concat(attributeValue.subs as never[]);
134 | }
135 | }
136 | } else {
137 | valueContent = true; // empty property conosidered bollean true
138 | }
139 | }
140 | if (attrName === '_props' && valueContent) {
141 | for (let propName in valueContent) {
142 | instance[propName] = valueContent[propName];
143 | instance._props[propName] = valueContent[propName];
144 | }
145 | } else {
146 | if (attribute.children?.length === 1 && attribute.children[0].content === 'false') {
147 | valueContent = false;
148 | }
149 | instance[attrName] = valueContent;
150 | instance._props[attrName] = valueContent;
151 | }
152 | // TODO: model
153 | // track
154 | if (valueSubs) {
155 | for (let subI in valueSubs) {
156 | const trackingPath = valueSubs[subI];
157 | track(parentInstance, trackingPath, props.scope, [updateProp, [instance, attribute, props]]);
158 | }
159 | }
160 | }
161 | }
162 | }
163 | if (!globalScope.cancel && info.hooks && info.hooks.mounted) {
164 | (instance as any).mounted();
165 | }
166 |
167 | // reuse && console.log(`Reusing component: ${name}`);
168 | if (name in globalScope.located) {
169 | globalScope.iteration[name] = { instance, scope, slots: {} };
170 | }
171 | if (reuse) {
172 | // console.log('Resue: Rendering slots');
173 | const slotHolders = lastIteration[name].slots;
174 | for (let slotName in slotHolders) {
175 | const anchorNode = slotHolders[slotName];
176 | if (anchorNode.parentNode && document.body.contains(anchorNode)) { // slot is visible on page
177 | if (slots && slotName in slots) { // slot has been passed
178 | const slot = slots[slotName];
179 | if (!slot.node.unpacked) {
180 | unpack(slot.node);
181 | slot.node.unpacked = true;
182 | }
183 | render(anchorNode, slot.scope.instance, slot.node.children!, slot.scope, undefined, false, true);
184 | } else {
185 | // TODO: render default slot content
186 | }
187 | globalScope.iteration[name].slots[slotName] = anchorNode;
188 | }
189 | }
190 | let componentName: string | false = name;
191 | while (componentName) {
192 | const componentInfo = componentsMeta.list[componentName];
193 | componentName = false;
194 | const componentRoot = componentInfo.nodes;
195 | if (componentRoot) {
196 | const rootChildren = componentRoot.children;
197 | if (rootChildren) {
198 | if (rootChildren[0].type === 'component' && rootChildren[0].content! in lastIteration) {
199 | globalScope.iteration[rootChildren[0].content!] = lastIteration[rootChildren[0].content!];
200 | componentName = rootChildren[0].content!;
201 | }
202 | }
203 | }
204 | }
205 | return scope;
206 | }
207 | // render
208 | if (info.renderer) {
209 | instance.render(target, name, scope, props, hydrate, insert, params);
210 | }
211 |
212 | if (
213 | target
214 | && root
215 | ) {
216 | if (!root.unpacked) {
217 | unpack(root);
218 | root.unpacked = true;
219 | }
220 | const rootChildren = root.children;
221 | if (rootChildren) {
222 | if (rootChildren[0].type === 'component') {
223 | globalScope.located[rootChildren[0].content!] = true;
224 | }
225 | // console.log(target, instance, rootChildren);
226 | rootChildren[0].first = true;
227 | render(target, instance, rootChildren, scope, undefined, hydrate, insert);
228 | // console.log(name, instance, rootChildren);
229 | // console.log(name, instance);
230 | }
231 | }
232 | if (info.hooks && info.hooks.rendered) {
233 | setTimeout(function () { (instance as any).rendered(); }, 0);
234 | }
235 | return scope;
236 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/render/renderDynamic.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { TemplateNode } from "../node/templateNode";
3 | import { PropsContext } from "../lifecycle/propsContext";
4 | import { render } from "./render";
5 | import { renderAttributeValue } from "./renderAttributeValue";
6 | import { renderComponent } from "./renderComponent";
7 | import { track } from "../reactivity/track";
8 | import { isComponent } from "../component/isComponent";
9 | import { TextAnchor } from "../anchor/textAnchor";
10 | import { ContextScope } from "../lifecycle/contextScope";
11 | import { dispose } from "../lifecycle/dispose";
12 |
13 | export function renderDynamic(instance: BaseComponent, node: TemplateNode, scopeContainer: { scope: ContextScope, anchorNode: TextAnchor }) {
14 | const content = node.expression
15 | ? instance.$$t[node.code as number](instance)
16 | : (node.content ?? '');
17 | const componentTag = node.type === "component"
18 | || (node.expression && isComponent(content));
19 | const anchorNode = scopeContainer.anchorNode;
20 | const scope = scopeContainer.scope.parent!;
21 | dispose(scopeContainer.scope);
22 | while (anchorNode.previousSibling._anchor !== anchorNode._anchor) {
23 | anchorNode.previousSibling!.remove();
24 | }
25 | const scopeId = ++scope.counter;
26 | const nextScope: ContextScope = {
27 | id: scopeId,
28 | why: 'dynamic',
29 | arguments: [...scope.arguments],
30 | map: { ...scope.map },
31 | track: [],
32 | instance: instance,
33 | lastComponent: scope.lastComponent,
34 | parent: scope,
35 | children: {},
36 | counter: 0,
37 | slots: scope.slots
38 | };
39 | if (scope.refs) {
40 | nextScope.refs = scope.refs;
41 | }
42 | scopeContainer.scope = nextScope;
43 | scope.children[scopeId] = nextScope;
44 | // component
45 | if (componentTag) {
46 | const slots = {};
47 | if (node.slots) {
48 | const scopeId = ++nextScope!.counter;
49 | const slotScope: ContextScope= {
50 | id: scopeId,
51 | why: 'slot',
52 | arguments: [...scope.arguments],
53 | map: { ...scope.map },
54 | track: [],
55 | instance: instance,
56 | lastComponent: scope.lastComponent,
57 | parent: nextScope,
58 | children: {},
59 | counter: 0,
60 | slots: scope.slots
61 | };
62 | for (let slotName in node.slots) {
63 | slots[slotName] = {
64 | node: node.slots[slotName],
65 | scope: slotScope
66 | };
67 | }
68 | }
69 | renderComponent(anchorNode, content, { attributes: node.attributes, scope: scope, instance: instance }, slots, false, true);
70 | return;
71 | } else {
72 | const element = anchorNode.parentElement!.insertBefore(document.createElement(content), anchorNode);
73 |
74 | if (node.attributes) {
75 | for (let a in node.attributes) {
76 | const attribute = node.attributes[a];
77 | const attrName = attribute.expression
78 | ? instance.$$t[attribute.code!](instance)
79 | : (attribute.content ?? '');
80 | if (attrName[0] === '(') {
81 | // event
82 | const eventName = attrName.substring(1, attrName.length - 1);
83 | if (attribute.children) {
84 | const eventHandler =
85 | instance.$$t[
86 | attribute.dynamic
87 | ? attribute.dynamic.code!
88 | : attribute.children[0].code!
89 | ](instance) as EventListener;
90 | element.addEventListener(eventName, eventHandler);
91 | // console.log('Event', attribute, eventName, eventHandler);
92 | }
93 | } else {
94 | renderAttributeValue(instance, attribute, element, attrName, nextScope);
95 | let valueSubs = []; // TODO: on backend, pass attribute value subs in attribute
96 | if (attribute.children) {
97 | for (let av in attribute.children) {
98 | const attributeValue = attribute.children[av];
99 | if (attributeValue.subs) {
100 | valueSubs = valueSubs.concat(attributeValue.subs as never[]);
101 | }
102 | }
103 | }
104 | if (valueSubs) {
105 | for (let subI in valueSubs) {
106 | const trackingPath = valueSubs[subI];
107 | track(instance, trackingPath, nextScope, [renderAttributeValue, [instance, attribute, element, attrName, nextScope]]);
108 | }
109 | }
110 | }
111 | }
112 | }
113 | if (node.children) {
114 | render(element, instance, node.children, nextScope, undefined, false, false);
115 | }
116 | }
117 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/render/renderForeach.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { TemplateNode } from "../node/templateNode";
3 | import { render } from "./render";
4 | import { DirectiveMap } from "../directive/directiveMap";
5 | import { createAnchorNode, nextAnchorNodeId } from "../anchor/createAnchorNode";
6 | import { TextAnchor } from "../anchor/textAnchor";
7 | import { ForeachAnchorEnum } from "../anchor/foreachAnchorEnum";
8 | import { ContextScope } from "../lifecycle/contextScope";
9 | import { ArrayScope } from "../lifecycle/arrayScope";
10 | import { dispose } from "../lifecycle/dispose";
11 |
12 | export function renderForeach(
13 | instance: BaseComponent,
14 | node: TemplateNode,
15 | directive: TemplateNode,
16 | anchors: { anchorBegin: TextAnchor, anchorNode: TextAnchor },
17 | currentArrayScope: ArrayScope,
18 | localDirectiveMap: DirectiveMap,
19 | scope: ContextScope
20 | ) {
21 | let callArguments = [instance];
22 | if (scope.arguments) {
23 | callArguments = callArguments.concat(scope.arguments);
24 | }
25 | const forMeta = directive.children![0];
26 | const noKey = !!forMeta.forKeyAuto;
27 | const data = instance.$$t[
28 | forMeta.forData!
29 | ].apply(null, callArguments);
30 |
31 | const isNumeric = Array.isArray(data);
32 | const usedMap = {};
33 | let positionIndex = -1;
34 | let moveBefore = anchors.anchorBegin.nextSibling;
35 | const nextArrayScope: ArrayScope = { data: {} };
36 | for (let forKey in data) {
37 | let found = false;
38 | positionIndex++;
39 | const dataKey = isNumeric ? +forKey : forKey;
40 | const dataItem = data[dataKey];
41 | let foundIndex = -1;
42 | for (let di in currentArrayScope.data) {
43 | foundIndex++;
44 | if (di in usedMap) {
45 | continue;
46 | }
47 | const currentScopeItem = currentArrayScope.data[di];
48 | if (currentScopeItem.value === dataItem && (noKey || currentScopeItem.key === dataKey)) {
49 | found = true;
50 | usedMap[di] = true;
51 | nextArrayScope.data[dataKey] = currentScopeItem;
52 | if (foundIndex !== positionIndex && moveBefore !== currentScopeItem.begin) {
53 | // move html
54 | const beginAnchor = currentScopeItem.begin;
55 | let nextToMove = beginAnchor.nextSibling;
56 | moveBefore.before(beginAnchor);
57 | while (nextToMove._anchor !== beginAnchor._anchor) {
58 | nextToMove = nextToMove.nextSibling;
59 | moveBefore.before(nextToMove.previousSibling);
60 | }
61 | moveBefore.before(nextToMove);
62 | }
63 | moveBefore = currentScopeItem.end.nextSibling;
64 | break;
65 | }
66 | }
67 | if (!found) {
68 | const scopeId = ++scope.counter;
69 | const nextScope: ContextScope = {
70 | id: scopeId,
71 | why: 'forItem',
72 | instance: instance,
73 | lastComponent: scope.lastComponent,
74 | arguments: [...scope.arguments],
75 | map: { ...scope.map },
76 | track: [],
77 | parent: scope,
78 | children: {},
79 | counter: 0
80 | };
81 | if (scope.refs) {
82 | nextScope.refs = scope.refs;
83 | }
84 | scope.children[scopeId] = nextScope;
85 | nextScope.map[directive.children![0].forKey!] = nextScope.arguments.length;
86 | nextScope.arguments.push(dataKey);
87 | nextScope.map[directive.children![0].forItem!] = nextScope.arguments.length;
88 | nextScope.arguments.push(dataItem);
89 | const nextDirectives: DirectiveMap = { map: { ...localDirectiveMap.map }, storage: { ...localDirectiveMap.storage } };
90 | const itemBeginAnchor = createAnchorNode(moveBefore, true, undefined, ForeachAnchorEnum.BeginAnchor + nextAnchorNodeId()); // begin foreach item
91 | render(moveBefore, instance, [node], nextScope, nextDirectives, false, true);
92 | const itemEndAnchor = createAnchorNode(moveBefore, true, undefined, itemBeginAnchor._anchor); // end foreach item
93 | moveBefore = itemEndAnchor.nextSibling;
94 | nextArrayScope.data[dataKey] = {
95 | key: dataKey,
96 | value: dataItem,
97 | begin: itemBeginAnchor,
98 | end: itemEndAnchor,
99 | scope: nextScope
100 | };
101 | }
102 | }
103 | // removing what's missing
104 | for (let di in currentArrayScope.data) {
105 | if (!(di in usedMap)) {
106 | const endAnchor = currentArrayScope.data[di].end;
107 | while (endAnchor.previousSibling._anchor !== endAnchor._anchor) {
108 | endAnchor.previousSibling!.remove();
109 | }
110 | currentArrayScope.data[di].begin.remove();
111 | endAnchor.remove();
112 | dispose(currentArrayScope.data[di].scope);
113 | delete currentArrayScope.data[di];
114 | }
115 | }
116 | currentArrayScope.data = nextArrayScope.data;
117 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/render/renderIf.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { TemplateNode } from "../node/templateNode";
3 | import { render } from "./render";
4 | import { ConditionalDirective } from "../directive/conditionalDirective";
5 | import { DirectiveMap } from "../directive/directiveMap";
6 | import { TextAnchor } from "../anchor/textAnchor";
7 | import { ContextScope } from "../lifecycle/contextScope";
8 | import { dispose } from "../lifecycle/dispose";
9 |
10 | export function renderIf(
11 | instance: BaseComponent,
12 | node: TemplateNode,
13 | scopeContainer: { scope: ContextScope, anchorNode: TextAnchor },
14 | directive: TemplateNode,
15 | ifConditions: ConditionalDirective,
16 | localDirectiveMap: DirectiveMap,
17 | index: number
18 | ) {
19 | let nextValue = true;
20 | for (let i = 0; i < index; i++) {
21 | nextValue = nextValue && !ifConditions.values[i];
22 | }
23 | const scope = scopeContainer.scope.parent!;
24 | if (directive.children) {
25 | let callArguments = [instance];
26 | if (scope.arguments) {
27 | callArguments = callArguments.concat(scope.arguments);
28 | }
29 | nextValue = nextValue && !!(instance.$$t[
30 | directive.children[0].code!
31 | ].apply(null, callArguments));
32 | }
33 | const anchorNode = scopeContainer.anchorNode;
34 | const nextDirectives: DirectiveMap = { map: { ...localDirectiveMap.map }, storage: { ...localDirectiveMap.storage } };
35 | if (ifConditions.values[index] !== nextValue) {
36 | ifConditions.values[index] = nextValue;
37 | if (nextValue) {
38 | // render
39 | const scopeId = ++scope.counter;
40 | const nextScope: ContextScope = {
41 | id: scopeId,
42 | why: index === 0 ? 'if' : (directive.children ? 'elseif' : 'else'),
43 | instance: instance,
44 | lastComponent: scope.lastComponent,
45 | arguments: [...scope.arguments],
46 | map: { ...scope.map },
47 | track: [],
48 | parent: scope,
49 | children: {},
50 | counter: 0,
51 | slots: scope.slots
52 | };
53 | if (scope.refs) {
54 | nextScope.refs = scope.refs;
55 | }
56 | scopeContainer.scope = nextScope;
57 | scope.children[scopeId] = nextScope;
58 | render(anchorNode, instance, [node], nextScope, nextDirectives, false, true);
59 | } else {
60 | // remove and dispose
61 | dispose(scopeContainer.scope);
62 | scopeContainer.scope = {
63 | id: -1,
64 | why: 'if',
65 | instance: instance,
66 | lastComponent: scope.lastComponent,
67 | arguments: [],
68 | map: {},
69 | track: [],
70 | parent: scope,
71 | children: {},
72 | counter: 0
73 | };
74 | while (anchorNode.previousSibling._anchor !== anchorNode._anchor) {
75 | anchorNode.previousSibling!.remove();
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/render/renderRaw.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { TextAnchor } from "../anchor/textAnchor";
3 | import { TemplateNode } from "../node/templateNode";
4 | import { ContextScope } from "../lifecycle/contextScope";
5 |
6 | export function renderRaw(instance: BaseComponent, node: TemplateNode, scope: ContextScope, anchorNode: TextAnchor) {
7 | // remove
8 | while (anchorNode.previousSibling._anchor !== anchorNode._anchor) {
9 | anchorNode.previousSibling!.remove();
10 | }
11 | // insert new content
12 | const parentTagNode = anchorNode.parentElement!;
13 | const vdom = document.createElement(parentTagNode.nodeName);
14 | let callArguments = [instance];
15 | if (scope.arguments) {
16 | callArguments = callArguments.concat(scope.arguments);
17 | }
18 | const content = (node.expression
19 | ? instance.$$t[node.code as number].apply(null, callArguments)
20 | : node.content) ?? '';
21 | vdom.innerHTML = content;
22 | const rawNodes = Array.prototype.slice.call(vdom.childNodes);
23 | for (let rawNodeI = 0; rawNodeI < rawNodes.length; rawNodeI++) {
24 | const rawNode = rawNodes[rawNodeI];
25 | parentTagNode.insertBefore(rawNode, anchorNode);
26 | }
27 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/render/renderText.ts:
--------------------------------------------------------------------------------
1 | import { BaseComponent } from "../component/baseComponent";
2 | import { ContextScope } from "../lifecycle/contextScope";
3 | import { TemplateNode } from "../node/templateNode";
4 |
5 | export function renderText(instance: BaseComponent, node: TemplateNode, textNode: Text, scope: ContextScope) {
6 | let callArguments = [instance];
7 | if (scope.arguments) {
8 | callArguments = callArguments.concat(scope.arguments);
9 | }
10 | const content = (node.expression
11 | ? instance.$$t[node.code as number].apply(null, callArguments)
12 | : node.content) ?? '';
13 | textNode.nodeValue !== content && (textNode.nodeValue = content);
14 | // debug purposes, TODO: debug/dev mode logs
15 | // if (textNode.parentNode && !document.body.contains(textNode)) {
16 | // console.log('Element is missing from the page', textNode);
17 | // }
18 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/router/handleUrl.ts:
--------------------------------------------------------------------------------
1 | import { componentsMeta } from "../component/componentsMeta";
2 | import { globalScope } from "../di/globalScope";
3 | import { renderApp } from "../render/renderApp";
4 | import { locationScope } from "./locationScope";
5 |
6 | const getPathName = function (href: string) {
7 | locationScope.link.href = href;
8 | return locationScope.link.pathname;
9 | };
10 |
11 | export const onUrlUpdate: {
12 | callback?: Function
13 | } = {};
14 |
15 | const updateHistory = function (href: string, forward: boolean = true) {
16 | if (forward) {
17 | window.history.pushState({ href: href }, '', href);
18 | }
19 | window.scrollTo(0, 0);
20 | onUrlUpdate.callback?.();
21 | }
22 |
23 | export function handleUrl(href: string, forward: boolean = true) {
24 | globalScope.cancel = true;
25 | const urlPath = getPathName(href);
26 | const routeItem = componentsMeta.router.resolve(urlPath);
27 | if (routeItem == null) {
28 | throw 'Can\'t resolve route for uri: ' + urlPath;
29 | }
30 | setTimeout(function () {
31 | renderApp(routeItem.item.action, routeItem.params, undefined, { func: updateHistory, href, forward });
32 | }, 0);
33 | }
34 |
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/router/locationScope.ts:
--------------------------------------------------------------------------------
1 | const htmlElementA = document.createElement('a');
2 |
3 | export const locationScope: { link: HTMLAnchorElement, scrollTo: string | null } = { link: htmlElementA, scrollTo: null };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/router/routeItem.ts:
--------------------------------------------------------------------------------
1 | export class RouteItem {
2 | method: string;
3 | url: string;
4 | action: string;
5 | wheres: { [key: string]: any };
6 | defaults: { [key: string]: any } | null = null;
7 |
8 | constructor(method: string, url: string, action: string, defaults: { [key: string]: any } | null = null, wheres?: { [key: string]: any }) {
9 | this.method = method;
10 | this.url = url;
11 | this.action = action;
12 | this.wheres = {};
13 | this.defaults = defaults;
14 | if (wheres) {
15 | this.wheres = wheres;
16 | }
17 | }
18 |
19 | where(wheresOrName: string | { [key: string]: any }, expr: any) {
20 | if (wheresOrName !== null && typeof wheresOrName === 'object') {
21 | this.wheres = Object.assign(this.where, wheresOrName);
22 | } else if (expr) {
23 | this.wheres[wheresOrName] = expr;
24 | }
25 | return this;
26 | };
27 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/router/routeRecord.ts:
--------------------------------------------------------------------------------
1 | import { RouteItem } from "./routeItem";
2 |
3 | export type RouteRecord = { item: RouteItem, params: { [key: string]: any } };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/router/router.ts:
--------------------------------------------------------------------------------
1 | import { RouteItem } from "./routeItem";
2 | import { RouteRecord } from "./routeRecord";
3 |
4 | export class Router {
5 | routes: RouteItem[];
6 | trimExpr: RegExp = /^\/|\/$/g;
7 |
8 | setRoutes(routeList: RouteItem[]) {
9 | this.routes = routeList;
10 | };
11 |
12 | getRoutes(): RouteItem[] {
13 | return this.routes;
14 | };
15 |
16 | register(
17 | method: string,
18 | url: string,
19 | action: string,
20 | defaults: { [key: string]: any } | null = null,
21 | wheres?: { [key: string]: any }
22 | ): RouteItem {
23 | const item = new RouteItem(
24 | method.toLowerCase(),
25 | url,
26 | action,
27 | defaults,
28 | wheres
29 | );
30 | this.routes.push(item);
31 | return item;
32 | };
33 |
34 | get(url: string, action: string): RouteItem {
35 | return this.register('get', url, action);
36 | };
37 |
38 | resolve(url: string): RouteRecord | null {
39 | url = url.replace(this.trimExpr, '');
40 | const parts = url.split('/');
41 | for (let k in this.routes) {
42 | const params = {};
43 | let valid = true;
44 | const item = this.routes[k];
45 | const targetUrl = item.url.replace(this.trimExpr, '');
46 | const targetParts = targetUrl.split('/');
47 | let pi = 0;
48 | let skipAll = false;
49 | for (pi; pi < targetParts.length; pi++) {
50 | const urlExpr = targetParts[pi];
51 | const hasWildCard = urlExpr.indexOf('*') !== -1;
52 | if (hasWildCard) {
53 | const beginning = urlExpr.slice(0, -1);
54 | if (!beginning || parts[pi].indexOf(beginning) === 0) {
55 | skipAll = true;
56 | break;
57 | }
58 | }
59 | const hasParams = urlExpr.indexOf('{') !== -1;
60 | if (urlExpr !== parts[pi] && !hasParams) {
61 | valid = false;
62 | break;
63 | }
64 | if (hasParams) {
65 | // has {***} parameter
66 | const bracketParts = urlExpr.split(/[{}]+/);
67 | // console.log(urlExpr, bracketParts);
68 | let paramName = bracketParts[1];
69 | if (paramName[paramName.length - 1] === '?') {
70 | // optional
71 | paramName = paramName.slice(0, -1);
72 | } else if (pi >= parts.length) {
73 | valid = false;
74 | break;
75 | }
76 | if (paramName.indexOf('<') !== -1) { // has
77 | const matches = /<([^>]+)>/.exec(paramName);
78 | if (matches) {
79 | paramName = paramName.replace(/<([^>]+)>/g, '');
80 | item.wheres[paramName] = matches[1];
81 | }
82 | }
83 | if (item.wheres[paramName]) {
84 | const regex = new RegExp(item.wheres[paramName], 'g');
85 | if (!regex.test(parts[pi])) {
86 | valid = false;
87 | break;
88 | }
89 | regex.lastIndex = 0;
90 | // test for "/"
91 | if (regex.test('/')) { // skip to the end
92 | skipAll = true;
93 | }
94 | }
95 | let paramValue = pi < parts.length ? parts[pi] : null;
96 | if (paramValue && bracketParts[0]) {
97 | if (paramValue.indexOf(bracketParts[0]) !== 0) {
98 | valid = false;
99 | break;
100 | } else {
101 | paramValue = paramValue.slice(bracketParts[0].length);
102 | }
103 | }
104 | params[paramName] = paramValue;
105 | if (skipAll) {
106 | params[paramName] = parts.slice(pi).join('/');
107 | break;
108 | }
109 | }
110 | }
111 | if (pi < parts.length && !skipAll) {
112 | valid = false;
113 | }
114 | if (valid) {
115 | return { item: item, params: params };
116 | }
117 | }
118 | return null;
119 | };
120 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/router/watchLinks.ts:
--------------------------------------------------------------------------------
1 | import { handleUrl } from "./handleUrl";
2 | import { locationScope } from "./locationScope";
3 |
4 | export function watchLinks() {
5 | document.addEventListener('click', function (event: MouseEvent) {
6 | if (event.defaultPrevented) {
7 | return;
8 | }
9 | if (!event.target) {
10 | console.warn('Can not aquire event target at "watchLinks".');
11 | }
12 | const target = event.target!;
13 | let nextTarget: HTMLLinkElement = target;
14 | while (nextTarget.parentElement && nextTarget.tagName !== 'A') {
15 | nextTarget = nextTarget.parentElement;
16 | }
17 | if (
18 | nextTarget.tagName === 'A'
19 | && nextTarget.href
20 | && nextTarget.href.indexOf(location.origin) === 0
21 | && (nextTarget.target === "_self" || !nextTarget.target)
22 | ) {
23 | locationScope.scrollTo = null;
24 | if (
25 | !locationScope.link.hash
26 | || locationScope.link.pathname !== location.pathname
27 | ) {
28 | event.preventDefault(); // Cancel native event
29 | // e.stopPropagation(); // Don't bubble/capture the event
30 | if (locationScope.link.hash) {
31 | locationScope.scrollTo = locationScope.link.hash;
32 | }
33 | handleUrl(nextTarget.href, true);
34 | }
35 | }
36 | }, false);
37 |
38 | // handle back button
39 | window.addEventListener('popstate', function (event) {
40 | if (event.state)
41 | handleUrl(event.state.href, false);
42 | else
43 | handleUrl(location.href, false);
44 | });
45 | }
--------------------------------------------------------------------------------
/viewi-app/js/viewi/core/viewi.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface Window { ViewiApp: { [name: string]: Viewi } }
3 | }
4 |
5 | export type Viewi = {
6 | register: { [name: string]: any },
7 | version: string,
8 | build: string,
9 | name: string,
10 | publish: (group: string, importComponents: {}) => void
11 | };
--------------------------------------------------------------------------------
/viewi-app/js/viewi/index.ts:
--------------------------------------------------------------------------------
1 | import { components, templates } from "../app/main/components";
2 | import { functions } from "../app/main/functions";
3 | import { resources } from "../app/main/resources";
4 | import "../modules/main";
5 | import { ComponentsJson } from "./core/component/componentsJson";
6 | import { componentsMeta } from "./core/component/componentsMeta";
7 | import { makeGlobal } from "./core/component/makeGlobal";
8 | import { delay } from "./core/di/delay";
9 | import { register } from "./core/di/register";
10 | import { setUp } from "./core/di/setUp";
11 | import { handleUrl } from "./core/router/handleUrl";
12 | import { watchLinks } from "./core/router/watchLinks";
13 | import { Viewi as ViewiApp } from "./core/viewi";
14 |
15 | const ViewiApp: ViewiApp = {
16 | register: {},
17 | version: resources.version,
18 | build: resources.build,
19 | name: resources.name,
20 | publish(group: string, importComponents: { [name: string]: any }) {
21 | for (let name in importComponents) {
22 | if (!(name in components)) {
23 | const imortItem = importComponents[name];
24 | if (imortItem._t === 'template') {
25 | componentsMeta.list[imortItem.name] = JSON.parse(imortItem.data);
26 | } else {
27 | components[name] = imortItem;
28 | }
29 | }
30 | }
31 | delay.ready(group);
32 | },
33 | };
34 |
35 | window.ViewiApp = window.ViewiApp || {};
36 | window.ViewiApp[resources.name] = ViewiApp;
37 |
38 | (async () => {
39 | let data: ComponentsJson = JSON.parse(templates);
40 | if (!resources.combine) {
41 | data = await (await fetch(resources.componentsPath)).json() as ComponentsJson;
42 | }
43 | componentsMeta.list = data;
44 | componentsMeta.router.setRoutes(data._routes);
45 | componentsMeta.config = data._config;
46 | componentsMeta.globals = data._globals;
47 | const booleanArray = data._meta['boolean'].split(',');
48 | for (let i = 0; i < booleanArray.length; i++) {
49 | componentsMeta.booleanAttributes[booleanArray[i]] = true;
50 | }
51 | setUp(data._startup);
52 | makeGlobal();
53 | ViewiApp.register = { ...components, ...register, ...functions };
54 | watchLinks();
55 | handleUrl(location.href);
56 | //setTimeout(() => renderApp('TestComponent'), 500);
57 | })();
--------------------------------------------------------------------------------
/viewi-app/routes.php:
--------------------------------------------------------------------------------
1 | router();
20 |
21 | $router->get('/', HomePage::class);
22 | $router->get('/counter', CounterPage::class);
23 | $router->get('/counter/{page}', CounterPage::class);
24 | $router->get('/todo', TodoAppPage::class);
25 | $router->get('/redirect-test', RedirectTestComponent::class);
26 | $router->get('/current-url', CurrentUrlTestPage::class);
27 | $router->get('/async-ssr-test/{id}', AsyncTestComponent::class);
28 | $router->get('/interceptors-test/{id}', InterceptorsTestComponent::class);
29 | $router->get('/middleware-test/{id}', MiddlewareTestComponent::class);
30 | $router->get('/middleware-fail-test/{id}', MiddlewareFailTestComponent::class);
31 | $router
32 | ->get('*', NotFoundPage::class)
33 | ->transform(function (Response $response) {
34 | return $response->withStatus(404, 'Not Found');
35 | });
36 |
--------------------------------------------------------------------------------
/viewi-app/viewi.php:
--------------------------------------------------------------------------------
1 |