Passed
Pull Request — master (#71)
by Wilmer
04:00
created

WebView::renderAjax()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 15
ccs 0
cts 11
cp 0
rs 9.9332
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 2
1
<?php
2
declare(strict_types = 1);
3
4
namespace Yiisoft\View;
5
6
use Yiisoft\Html\Html;
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\View\Event\BodyBegin;
9
use Yiisoft\View\Event\BodyEnd;
10
use Yiisoft\View\Event\PageEnd;
11
12
/**
13
 * View represents a view object in the MVC pattern.
14
 *
15
 * View provides a set of methods (e.g. {@see render()} for rendering purpose.
16
 *
17
 * You can modify its configuration by adding an array to your application config under `components` as it is shown in
18
 * the following example:
19
 *
20
 * ```php
21
 * 'view' => [
22
 *     'theme' => 'app\themes\MyTheme',
23
 *     'renderers' => [
24
 *         // you may add Smarty or Twig renderer here
25
 *     ]
26
 *     // ...
27
 * ]
28
 * ```
29
 *
30
 * For more details and usage information on View, see the [guide article on views](guide:structure-views).
31
 */
32
class WebView extends View
33
{
34
    /**
35
     * The location of registered JavaScript code block or files.
36
     * This means the location is in the head section.
37
     */
38
    public const POSITION_HEAD = 1;
39
40
    /**
41
     * The location of registered JavaScript code block or files.
42
     * This means the location is at the beginning of the body section.
43
     */
44
    public const POSITION_BEGIN = 2;
45
46
    /**
47
     * The location of registered JavaScript code block or files.
48
     * This means the location is at the end of the body section.
49
     */
50
    public const POSITION_END = 3;
51
52
    /**
53
     * The location of registered JavaScript code block.
54
     * This means the JavaScript code block will be executed when HTML document composition is ready.
55
     */
56
    public const POSITION_READY = 4;
57
58
    /**
59
     * The location of registered JavaScript code block.
60
     * This means the JavaScript code block will be executed when HTML page is completely loaded.
61
     */
62
    public const POSITION_LOAD = 5;
63
64
    /**
65
     * This is internally used as the placeholder for receiving the content registered for the head section.
66
     */
67
    private const PLACEHOLDER_HEAD = '<![CDATA[YII-BLOCK-HEAD]]>';
68
69
    /**
70
     * This is internally used as the placeholder for receiving the content registered for the beginning of the body
71
     * section.
72
     */
73
    private const PLACEHOLDER_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN]]>';
74
75
    /**
76
     * This is internally used as the placeholder for receiving the content registered for the end of the body section.
77
     */
78
    private const PLACEHOLDER_BODY_END = '<![CDATA[YII-BLOCK-BODY-END]]>';
79
80
    /**
81
     * @var string the page title
82
     */
83
    private string $title;
84
85
    /**
86
     * @var array the registered meta tags.
87
     *
88
     * {@see registerMetaTag()}
89
     */
90
    private array $metaTags = [];
91
92
    /**
93
     * @var array the registered link tags.
94
     *
95
     * {@see registerLinkTag()}
96
     */
97
    private array $linkTags = [];
98
99
    /**
100
     * @var array the registered CSS code blocks.
101
     *
102
     * {@see registerCss()}
103
     */
104
    private array $css = [];
105
106
    /**
107
     * @var array the registered CSS files.
108
     *
109
     * {@see registerCssFile()}
110
     */
111
    private array $cssFiles = [];
112
113
    /**
114
     * @var array the registered JS code blocks
115
     *
116
     * {@see registerJs()}
117
     */
118
    private array $js = [];
119
120
    /**
121
     * @var array the registered JS files.
122
     *
123
     * {@see registerJsFile()}
124
     */
125
    private array $jsFiles = [];
126
127
    /**
128
     * Marks the position of an HTML head section.
129
     */
130 4
    public function head(): void
131
    {
132 4
        echo self::PLACEHOLDER_HEAD;
133
    }
134
135
    /**
136
     * Marks the beginning of an HTML body section.
137
     */
138 4
    public function beginBody(): void
139
    {
140 4
        echo self::PLACEHOLDER_BODY_BEGIN;
141 4
        $this->eventDispatcher->dispatch(new BodyBegin($this->getViewFile()));
142
    }
143
144
    /**
145
     * Marks the ending of an HTML body section.
146
     */
147 4
    public function endBody(): void
148
    {
149 4
        $this->eventDispatcher->dispatch(new BodyEnd($this->getViewFile()));
150 4
        echo self::PLACEHOLDER_BODY_END;
151
    }
152
153
    /**
154
     * Marks the ending of an HTML page.
155
     *
156
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
157
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like
158
     * normal scripts.
159
     */
160 4
    public function endPage($ajaxMode = false): void
161
    {
162 4
        $this->eventDispatcher->dispatch(new PageEnd($this->getViewFile()));
163
164 4
        $content = ob_get_clean();
165
166 4
        echo strtr($content, [
167 4
            self::PLACEHOLDER_HEAD => $this->renderHeadHtml(),
168 4
            self::PLACEHOLDER_BODY_BEGIN => $this->renderBodyBeginHtml(),
169 4
            self::PLACEHOLDER_BODY_END => $this->renderBodyEndHtml($ajaxMode),
170
        ]);
171
172 4
        $this->clear();
173
    }
174
175
    /**
176
     * Renders a view in response to an AJAX request.
177
     *
178
     * This method is similar to {@see render()} except that it will surround the view being rendered with the calls of
179
     * {@see beginPage()}, {@see head()}, {@see beginBody()}, {@see endBody()} and {@see endPage()}. By doing so, the
180
     * method is able to inject into the rendering result with JS/CSS scripts and files that are registered with the
181
     * view.
182
     *
183
     * @param string $view the view name. Please refer to {@see render()} on how to specify this parameter.
184
     * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view
185
     * file.
186
     * @param ViewContextInterface|null $context the context that the view should use for rendering the view. If null,
187
     * existing {@see context} will be used.
188
     *
189
     * @return string the rendering result
190
     *
191
     * {@see render()}
192
     */
193
    public function renderAjax(string $view, array $params = [], ?ViewContextInterface $context = null): string
194
    {
195
        $viewFile = $this->findTemplateFile($view, $context);
196
197
        ob_start();
198
        ob_implicit_flush(0);
199
200
        $this->beginPage();
201
        $this->head();
202
        $this->beginBody();
203
        echo $this->renderFile($viewFile, $params, $context);
204
        $this->endBody();
205
        $this->endPage(true);
206
207
        return ob_get_clean();
208
    }
209
210
    /**
211
     * Clears up the registered meta tags, link tags, css/js scripts and files.
212
     *
213
     * @return void
214
     */
215 4
    public function clear(): void
216
    {
217 4
        $this->metaTags = [];
218 4
        $this->linkTags = [];
219 4
        $this->css = [];
220 4
        $this->cssFiles = [];
221 4
        $this->js = [];
222 4
        $this->jsFiles = [];
223
    }
224
225
    /**
226
     * Registers a meta tag.
227
     *
228
     * For example, a description meta tag can be added like the following:
229
     *
230
     * ```php
231
     * $view->registerMetaTag([
232
     *     'name' => 'description',
233
     *     'content' => 'This website is about funny raccoons.'
234
     * ]);
235
     * ```
236
     *
237
     * will result in the meta tag `<meta name="description" content="This website is about funny raccoons.">`.
238
     *
239
     * @param array $options the HTML attributes for the meta tag.
240
     * @param string $key the key that identifies the meta tag. If two meta tags are registered with the same key, the
241
     * latter will overwrite the former. If this is null, the new meta tag will be appended to the
242
     * existing ones.
243
     *
244
     * @return void
245
     */
246
    public function registerMetaTag(array $options, string $key = null): void
247
    {
248
        if ($key === null) {
249
            $this->metaTags[] = Html::tag('meta', '', $options);
250
        } else {
251
            $this->metaTags[$key] = Html::tag('meta', '', $options);
252
        }
253
    }
254
255
    /**
256
     * Registers a link tag.
257
     *
258
     * For example, a link tag for a custom [favicon](http://www.w3.org/2005/10/howto-favicon) can be added like the
259
     * following:
260
     *
261
     * ```php
262
     * $view->registerLinkTag(['rel' => 'icon', 'type' => 'image/png', 'href' => '/myicon.png']);
263
     * ```
264
     *
265
     * which will result in the following HTML: `<link rel="icon" type="image/png" href="/myicon.png">`.
266
     *
267
     * **Note:** To register link tags for CSS stylesheets, use {@see registerCssFile()]} instead, which has more
268
     * options for this kind of link tag.
269
     *
270
     * @param array $options the HTML attributes for the link tag.
271
     * @param string|null $key the key that identifies the link tag. If two link tags are registered with the same
272
     * key, the latter will overwrite the former. If this is null, the new link tag will be appended
273
     * to the existing ones.
274
     */
275
    public function registerLinkTag(array $options, ?string $key = null): void
276
    {
277
        if ($key === null) {
278
            $this->linkTags[] = Html::tag('link', '', $options);
279
        } else {
280
            $this->linkTags[$key] = Html::tag('link', '', $options);
281
        }
282
    }
283
284
    /**
285
     * Registers CSRF meta tags.
286
     *
287
     * They are rendered dynamically to retrieve a new CSRF token for each request.
288
     *
289
     * ```php
290
     * $view->registerCsrfMetaTags();
291
     * ```
292
     *
293
     * The above code will result in `<meta name="csrf-param" content="[\Yiisoft\Web\Request::$csrfParam]">` and
294
     * `<meta name="csrf-token" content="tTNpWKpdy-bx8ZmIq9R72...K1y8IP3XGkzZA==">` added to the page.
295
     *
296
     * Note: Hidden CSRF input of ActiveForm will be automatically refreshed by calling `window.yii.refreshCsrfToken()`
297
     * from `yii.js`.
298
     */
299
    public function registerCsrfMetaTags(): void
300
    {
301
        $this->metaTags['csrf_meta_tags'] = $this->renderDynamic('return Yiisoft\Html\Html::csrfMetaTags();');
302
    }
303
304
    /**
305
     * Registers a CSS code block.
306
     *
307
     * @param string $css the content of the CSS code block to be registered
308
     * @param array $options the HTML attributes for the `<style>`-tag.
309
     * @param string $key the key that identifies the CSS code block. If null, it will use $css as the key. If two CSS
310
     * code blocks are registered with the same key, the latter will overwrite the former.
311
     */
312
    public function registerCss(string $css, array $options = [], string $key = null): void
313
    {
314
        $key = $key ?: md5($css);
315
        $this->css[$key] = Html::style($css, $options);
316
    }
317
318
    /**
319
     * Registers a CSS file.
320
     *
321
     * This method should be used for simple registration of CSS files. If you want to use features of
322
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
323
     * {@see \Yiisoft\Assets\AssetBundle}.
324
     *
325
     * @param string $url the CSS file to be registered.
326
     * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
327
     * for the supported options.
328
     *
329
     * @param string $key the key that identifies the CSS script file. If null, it will use $url as the key. If two CSS
330
     * files are registered with the same key, the latter will overwrite the former.
331
     *
332
     * @return void
333
     */
334 1
    public function registerCssFile(string $url, array $options = [], string $key = null): void
335
    {
336 1
        $key = $key ?: $url;
337
338 1
        $this->cssFiles[$key] = Html::cssFile($url, $options);
339
    }
340
341
    /**
342
     * Registers a JS code block.
343
     *
344
     * @param string $js the JS code block to be registered
345
     * @param int $position the position at which the JS script tag should be inserted in a page.
346
     *
347
     * The possible values are:
348
     *
349
     * - {@see POSITION_HEAD}: in the head section
350
     * - {@see POSITION_BEGIN}: at the beginning of the body section
351
     * - {@see POSITION_END}: at the end of the body section. This is the default value.
352
     * - {@see POSITION_LOAD}: executed when HTML page is completely loaded.
353
     * - {@see POSITION_READY}: executed when HTML document composition is ready.
354
     *
355
     * @param string $key the key that identifies the JS code block. If null, it will use $js as the key. If two JS code
356
     * blocks are registered with the same key, the latter will overwrite the former.
357
     *
358
     * @return void
359
     */
360 1
    public function registerJs(string $js, int $position = self::POSITION_END, string $key = null): void
361
    {
362 1
        $key = $key ?: md5($js);
363 1
        $this->js[$position][$key] = $js;
364
    }
365
366
    /**
367
     * Registers a JS file.
368
     *
369
     * This method should be used for simple registration of JS files. If you want to use features of
370
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
371
     * {@see \Yiisoft\Assets\AssetBundle}.
372
     *
373
     * @param string $url the JS file to be registered.
374
     * @param array $options the HTML attributes for the script tag. The following options are specially handled and
375
     * are not treated as HTML attributes:
376
     *
377
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
378
     *     * {@see POSITION_HEAD}: in the head section
379
     *     * {@see POSITION_BEGIN}: at the beginning of the body section
380
     *     * {@see POSITION_END}: at the end of the body section. This is the default value.
381
     *
382
     * Please refer to {@see \Yiisoft\Html\Html::jsFile()} for other supported options.
383
     *
384
     * @param string $key the key that identifies the JS script file. If null, it will use $url as the key. If two JS
385
     * files are registered with the same key at the same position, the latter will overwrite the former.
386
     * Note that position option takes precedence, thus files registered with the same key, but different
387
     * position option will not override each other.
388
     *
389
     * @return void
390
     */
391 1
    public function registerJsFile(string $url, array $options = [], string $key = null): void
392
    {
393 1
        $key = $key ?: $url;
394
395 1
        $position = ArrayHelper::remove($options, 'position', self::POSITION_END);
396 1
        $this->jsFiles[$position][$key] = Html::jsFile($url, $options);
397
    }
398
399
    /**
400
     * Registers a JS code block defining a variable. The name of variable will be used as key, preventing duplicated
401
     * variable names.
402
     *
403
     * @param string $name Name of the variable
404
     * @param array|string $value Value of the variable
405
     * @param int $position the position in a page at which the JavaScript variable should be inserted.
406
     *
407
     * The possible values are:
408
     *
409
     * - {@see POSITION_HEAD}: in the head section. This is the default value.
410
     * - {@see POSITION_BEGIN}: at the beginning of the body section.
411
     * - {@see POSITION_END}: at the end of the body section.
412
     * - {@see POSITION_LOAD}: enclosed within jQuery(window).load().
413
     *   Note that by using this position, the method will automatically register the jQuery js file.
414
     * - {@see POSITION_READY}: enclosed within jQuery(document).ready().
415
     *   Note that by using this position, the method will automatically register the jQuery js file.
416
     */
417 1
    public function registerJsVar(string $name, $value, int $position = self::POSITION_HEAD): void
418
    {
419 1
        $js = sprintf('var %s = %s;', $name, \Yiisoft\Json\Json::htmlEncode($value));
420 1
        $this->registerJs($js, $position, $name);
421
    }
422
423
    /**
424
     * Renders the content to be inserted in the head section.
425
     *
426
     * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
427
     *
428
     * @return string the rendered content
429
     */
430 4
    protected function renderHeadHtml(): string
431
    {
432 4
        $lines = [];
433 4
        if (!empty($this->metaTags)) {
434
            $lines[] = implode("\n", $this->metaTags);
435
        }
436
437 4
        if (!empty($this->linkTags)) {
438
            $lines[] = implode("\n", $this->linkTags);
439
        }
440 4
        if (!empty($this->cssFiles)) {
441 1
            $lines[] = implode("\n", $this->cssFiles);
442
        }
443 4
        if (!empty($this->css)) {
444
            $lines[] = implode("\n", $this->css);
445
        }
446 4
        if (!empty($this->jsFiles[self::POSITION_HEAD])) {
447 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_HEAD]);
448
        }
449 4
        if (!empty($this->js[self::POSITION_HEAD])) {
450 1
            $lines[] = Html::script(implode("\n", $this->js[self::POSITION_HEAD]));
451
        }
452
453 4
        return empty($lines) ? '' : implode("\n", $lines);
454
    }
455
456
    /**
457
     * Renders the content to be inserted at the beginning of the body section.
458
     *
459
     * The content is rendered using the registered JS code blocks and files.
460
     *
461
     * @return string the rendered content
462
     */
463 4
    protected function renderBodyBeginHtml(): string
464
    {
465 4
        $lines = [];
466 4
        if (!empty($this->jsFiles[self::POSITION_BEGIN])) {
467 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_BEGIN]);
468
        }
469 4
        if (!empty($this->js[self::POSITION_BEGIN])) {
470
            $lines[] = Html::script(implode("\n", $this->js[self::POSITION_BEGIN]));
471
        }
472
473 4
        return empty($lines) ? '' : implode("\n", $lines);
474
    }
475
476
    /**
477
     * Renders the content to be inserted at the end of the body section.
478
     *
479
     * The content is rendered using the registered JS code blocks and files.
480
     *
481
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
482
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like normal
483
     * scripts.
484
     *
485
     * @return string the rendered content
486
     */
487 4
    protected function renderBodyEndHtml(bool $ajaxMode): string
488
    {
489 4
        $lines = [];
490
491 4
        if (!empty($this->jsFiles[self::POSITION_END])) {
492 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_END]);
493
        }
494
495 4
        if ($ajaxMode) {
496
            $scripts = [];
497
            if (!empty($this->js[self::POSITION_END])) {
498
                $scripts[] = implode("\n", $this->js[self::POSITION_END]);
499
            }
500
            if (!empty($this->js[self::POSITION_READY])) {
501
                $scripts[] = implode("\n", $this->js[self::POSITION_READY]);
502
            }
503
            if (!empty($this->js[self::POSITION_LOAD])) {
504
                $scripts[] = implode("\n", $this->js[self::POSITION_LOAD]);
505
            }
506
            if (!empty($scripts)) {
507
                $lines[] = Html::script(implode("\n", $scripts));
508
            }
509
        } else {
510 4
            if (!empty($this->js[self::POSITION_END])) {
511
                $lines[] = Html::script(implode("\n", $this->js[self::POSITION_END]));
512
            }
513 4
            if (!empty($this->js[self::POSITION_READY])) {
514
                $js = "document.addEventListener('DOMContentLoaded', function(event) {\n" . implode("\n", $this->js[self::POSITION_READY]) . "\n});";
515
                $lines[] = Html::script($js, ['type' => 'text/javascript']);
516
            }
517 4
            if (!empty($this->js[self::POSITION_LOAD])) {
518
                $js = "window.addEventListener('load', function (event) {\n" . implode("\n", $this->js[self::POSITION_LOAD]) . "\n});";
519
                $lines[] = Html::script($js, ['type' => 'text/javascript']);
520
            }
521
        }
522
523 4
        return empty($lines) ? '' : implode("\n", $lines);
524
    }
525
526
    /**
527
     * Get title in views.
528
     *
529
     *
530
     * in Layout:
531
     *
532
     * ```php
533
     * <title><?= Html::encode($this->getTitle()) ?></title>
534
     * ```
535
     *
536
     * in Views:
537
     *
538
     * ```php
539
     * $this->setTitle('Web Application - Yii 3.0.');
540
     * ```
541
     *
542
     * @return string
543
     */
544
    public function getTitle(): string
545
    {
546
        return $this->title;
547
    }
548
549
    /**
550
     * It processes the CSS configuration generated by the asset manager and converts it into HTML code.
551
     *
552
     * @param array $cssFiles
553
     * @return void
554
     */
555
    public function setCssFiles(array $cssFiles): void
556
    {
557
        foreach ($cssFiles as $key => $value) {
558
            $this->registerCssFile(
559
                $cssFiles[$key]['url'],
560
                $cssFiles[$key]['attributes']
561
            );
562
        }
563
    }
564
565
    /**
566
     * It processes the JS configuration generated by the asset manager and converts it into HTML code.
567
     *
568
     * @param array $jsFiles
569
     * @return void
570
     */
571
    public function setJsFiles(array $jsFiles): void
572
    {
573
        foreach ($jsFiles as $key => $value) {
574
            $this->registerJsFile(
575
                $jsFiles[$key]['url'],
576
                $jsFiles[$key]['attributes']
577
            );
578
        }
579
    }
580
581
    /**
582
     * Set title in views.
583
     *
584
     * {@see getTitle()}
585
     *
586
     * @param string $value
587
     * @return void
588
     */
589
    public function setTitle($value): void
590
    {
591
        $this->title = $value;
592
    }
593
}
594