Skip to content

Commit 8ac2da9

Browse files
author
epriestley
committed
Provide hasChildren() to replace isEmptyContent()
Summary: Fixes T3698. Sometimes views need to render differently depending on whether they contain content or not. The existing approach for this is `isEmptyContent()`, which doesn't work well and is sort of hacky (it implies double-rendering content, which is not always free or side-effect free). Instead, provide a test for an element without children. This test is powerful enough to catch the easy cases of `null`, etc., and just do the expected thing, but will not catch a View which is reduced upon rendering. Since this is rare and we have no actual need for it today, just accept that as a limitation. Test Plan: Viewed Timeline and Feed UI examples. Viewed Feed (feed), Pholio (timelineview), and Differential (old transactionview). {F53915} Reviewers: chad, btrahan Reviewed By: chad CC: aran Maniphest Tasks: T3698 Differential Revision: https://secure.phabricator.com/D6718
1 parent 52225f7 commit 8ac2da9

File tree

7 files changed

+128
-25
lines changed

7 files changed

+128
-25
lines changed

src/__phutil_library_map__.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,7 @@
775775
'PhabricatorAllCapsTranslation' => 'infrastructure/internationalization/PhabricatorAllCapsTranslation.php',
776776
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
777777
'PhabricatorAphrontBarExample' => 'applications/uiexample/examples/PhabricatorAphrontBarExample.php',
778+
'PhabricatorAphrontViewTestCase' => 'view/__tests__/PhabricatorAphrontViewTestCase.php',
778779
'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php',
779780
'PhabricatorApplicationApplications' => 'applications/meta/application/PhabricatorApplicationApplications.php',
780781
'PhabricatorApplicationAudit' => 'applications/audit/application/PhabricatorApplicationAudit.php',
@@ -2804,6 +2805,7 @@
28042805
'PhabricatorAllCapsTranslation' => 'PhabricatorTranslation',
28052806
'PhabricatorAnchorView' => 'AphrontView',
28062807
'PhabricatorAphrontBarExample' => 'PhabricatorUIExample',
2808+
'PhabricatorAphrontViewTestCase' => 'PhabricatorTestCase',
28072809
'PhabricatorApplicationApplications' => 'PhabricatorApplication',
28082810
'PhabricatorApplicationAudit' => 'PhabricatorApplication',
28092811
'PhabricatorApplicationAuth' => 'PhabricatorApplication',

src/view/AphrontNullView.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
final class AphrontNullView extends AphrontView {
44

55
public function render() {
6-
return phutil_implode_html('', $this->renderChildren());
6+
return $this->renderChildren();
77
}
88

99
}

src/view/AphrontView.php

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,143 @@
11
<?php
22

3+
/**
4+
* @task children Managing Children
5+
*/
36
abstract class AphrontView extends Phobject
47
implements PhutilSafeHTMLProducerInterface {
58

69
protected $user;
710
protected $children = array();
811

12+
13+
/* -( Configuration )------------------------------------------------------ */
14+
15+
16+
/**
17+
* @task config
18+
*/
919
public function setUser(PhabricatorUser $user) {
1020
$this->user = $user;
1121
return $this;
1222
}
1323

24+
25+
/**
26+
* @task config
27+
*/
1428
protected function getUser() {
1529
return $this->user;
1630
}
1731

32+
33+
/* -( Managing Children )-------------------------------------------------- */
34+
35+
36+
/**
37+
* Test if this View accepts children.
38+
*
39+
* By default, views accept children, but subclases may override this method
40+
* to prevent children from being appended. Doing so will cause
41+
* @{method:appendChild} to throw exceptions instead of appending children.
42+
*
43+
* @return bool True if the View should accept children.
44+
* @task children
45+
*/
1846
protected function canAppendChild() {
1947
return true;
2048
}
2149

50+
51+
/**
52+
* Append a child to the list of children.
53+
*
54+
* This method will only work if the view supports children, which is
55+
* determined by @{method:canAppendChild}.
56+
*
57+
* @param wild Something renderable.
58+
* @return this
59+
*/
2260
final public function appendChild($child) {
2361
if (!$this->canAppendChild()) {
2462
$class = get_class($this);
2563
throw new Exception(
2664
pht("View '%s' does not support children.", $class));
2765
}
66+
2867
$this->children[] = $child;
68+
2969
return $this;
3070
}
3171

72+
73+
/**
74+
* Produce children for rendering.
75+
*
76+
* Historically, this method reduced children to a string representation,
77+
* but it no longer does.
78+
*
79+
* @return wild Renderable children.
80+
* @task
81+
*/
3282
final protected function renderChildren() {
3383
return $this->children;
3484
}
3585

86+
3687
/**
37-
* @deprecated
88+
* Test if an element has no children.
89+
*
90+
* @return bool True if this element has children.
91+
* @task children
3892
*/
39-
final protected function renderSingleView($child) {
40-
phutil_deprecated(
41-
'AphrontView->renderSingleView()',
42-
"This method no longer does anything; it can be removed and replaced ".
43-
"with its arguments.");
44-
return $child;
93+
final public function hasChildren() {
94+
if ($this->children) {
95+
$this->children = $this->reduceChildren($this->children);
96+
}
97+
return (bool)$this->children;
4598
}
4699

47-
final protected function isEmptyContent($content) {
48-
if (is_array($content)) {
49-
foreach ($content as $element) {
50-
if (!$this->isEmptyContent($element)) {
51-
return false;
100+
101+
/**
102+
* Reduce effectively-empty lists of children to be actually empty. This
103+
* recursively removes `null`, `''`, and `array()` from the list of children
104+
* so that @{method:hasChildren} can more effectively align with expectations.
105+
*
106+
* NOTE: Because View children are not rendered, a View which renders down
107+
* to nothing will not be reduced by this method.
108+
*
109+
* @param list<wild> Renderable children.
110+
* @return list<wild> Reduced list of children.
111+
* @task children
112+
*/
113+
private function reduceChildren(array $children) {
114+
foreach ($children as $key => $child) {
115+
if ($child === null) {
116+
unset($children[$key]);
117+
} else if ($child === '') {
118+
unset($children[$key]);
119+
} else if (is_array($child)) {
120+
$child = $this->reduceChildren($child);
121+
if ($child) {
122+
$children[$key] = $child;
123+
} else {
124+
unset($children[$key]);
52125
}
53126
}
54-
return true;
55-
} else {
56-
return !strlen((string)$content);
57127
}
128+
return $children;
58129
}
59130

131+
132+
/* -( Rendering )---------------------------------------------------------- */
133+
134+
60135
abstract public function render();
61136

137+
138+
/* -( PhutilSafeHTMLProducerInterface )------------------------------------ */
139+
140+
62141
public function producePhutilSafeHTML() {
63142
return $this->render();
64143
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
final class PhabricatorAphrontViewTestCase extends PhabricatorTestCase {
4+
5+
public function testHasChildren() {
6+
$view = new AphrontNullView();
7+
$this->assertEqual(false, $view->hasChildren());
8+
9+
$values = array(
10+
null,
11+
'',
12+
array(),
13+
array(null, ''),
14+
);
15+
16+
foreach ($values as $value) {
17+
$view->appendChild($value);
18+
$this->assertEqual(false, $view->hasChildren());
19+
}
20+
21+
$view->appendChild("!");
22+
$this->assertEqual(true, $view->hasChildren());
23+
}
24+
25+
}

src/view/layout/PhabricatorTimelineEventView.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,8 @@ public function setColor($color) {
100100
}
101101

102102
public function render() {
103-
$content = $this->renderChildren();
104-
105103
$title = $this->title;
106-
if (($title === null) && $this->isEmptyContent($content)) {
104+
if (($title === null) && !$this->hasChildren()) {
107105
$title = '';
108106
}
109107

@@ -163,7 +161,7 @@ public function render() {
163161
$classes = array();
164162
$classes[] = 'phabricator-timeline-event-view';
165163
$classes[] = 'phabricator-timeline-border';
166-
if (!$this->isEmptyContent($content)) {
164+
if ($this->hasChildren()) {
167165
$classes[] = 'phabricator-timeline-major-event';
168166
$content = phutil_tag(
169167
'div',
@@ -182,7 +180,7 @@ public function render() {
182180
array(
183181
'class' => 'phabricator-timeline-core-content',
184182
),
185-
$content),
183+
$this->renderChildren()),
186184
)));
187185
$content = array($image, $wedge, $content);
188186
} else {

src/view/layout/PhabricatorTransactionView.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,13 @@ private function renderTransactionStyle() {
136136
}
137137

138138
private function renderTransactionContent() {
139-
$content = $this->renderChildren();
140-
if ($this->isEmptyContent($content)) {
139+
if (!$this->hasChildren()) {
141140
return null;
142141
}
143142
return phutil_tag(
144143
'div',
145144
array('class' => 'phabricator-transaction-content'),
146-
$content);
145+
$this->renderChildren());
147146
}
148147

149148
}

src/view/phui/PHUIFeedStoryView.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public function render() {
124124

125125
require_celerity_resource('phui-feed-story-css');
126126
Javelin::initBehavior('phabricator-hovercards');
127-
$oneline = $this->isEmptyContent($this->renderChildren());
127+
$oneline = !$this->hasChildren();
128128

129129
$body = null;
130130
$foot = null;

0 commit comments

Comments
 (0)