Skip to content
This repository was archived by the owner on Apr 1, 2024. It is now read-only.

Commit 3923d04

Browse files
committed
Automatically call transferAttributes as needed.
- split out XHPBaseHTMLHelpers to just implement the ID and class helpers without transferAttributes - added tests - XHPHelpers should probably be renamed to XHPHTMLHelpers in XHP-Lib 3, and the bulk of the transfer/copyattribtues functiontionality split to XHPTransferAttributes or something like that. Keeping like this for now to reduce BC breakage - Allows implementation of non-HTML helpers; until the above modification happens this will involve copypasta, but better than the current situation. - I /think/ this gets rid of the render()/compose() pattern. fixes #130 closes #133
1 parent d7b472f commit 3923d04

File tree

8 files changed

+287
-57
lines changed

8 files changed

+287
-57
lines changed

autoload-map.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
function autoload($class): bool {
1313
$classmap = Map {
14+
'HasXHPBaseHTMLHelpers' => '/src/html/HasXHPBaseHTMLHelpers.php',
1415
'HasXHPHelpers' => '/src/html/XHPHelpers.php',
1516
'ReflectionXHPAttribute' => '/src/core/ReflectionXHPAttribute.php',
1617
'ReflectionXHPChildrenDeclaration' => '/src/core/ReflectionXHPChildrenDeclaration.php',
@@ -24,12 +25,14 @@ function autoload($class): bool {
2425
'XHPAttributeRequiredException' => '/src/exceptions/AttributeRequiredException.php',
2526
'XHPAttributeType' => '/src/core/ReflectionXHPAttribute.php',
2627
'XHPAwaitable' => '/src/core/XHPAwaitable.php',
28+
'XHPBaseHTMLHelpers' => '/src/html/XHPBaseHTMLHelpers.php',
2729
'XHPChildrenConstraintType' => '/src/core/ReflectionXHPChildrenDeclaration.php',
2830
'XHPChildrenDeclarationType' => '/src/core/ReflectionXHPChildrenDeclaration.php',
2931
'XHPChildrenExpressionType' => '/src/core/ReflectionXHPChildrenDeclaration.php',
3032
'XHPClassException' => '/src/exceptions/ClassException.php',
3133
'XHPCoreRenderException' => '/src/exceptions/CoreRenderException.php',
3234
'XHPException' => '/src/exceptions/Exception.php',
35+
'XHPHasTransferAttributes' => '/src/core/XHPHasTransferAttributes.php',
3336
'XHPHelpers' => '/src/html/XHPHelpers.php',
3437
'XHPInvalidArrayAttributeException' => '/src/exceptions/InvalidArrayAttributeException.php',
3538
'XHPInvalidArrayKeyAttributeException' => '/src/exceptions/InvalidArrayKeyAttributeException.php',

src/core/Element.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ final public function toString(): string {
4747
}
4848
assert($composed instanceof :x:element);
4949
$composed->__transferContext($that->getAllContexts());
50+
if ($that instanceof XHPHasTransferAttributes) {
51+
$that->transferAttributesToRenderedRoot($composed);
52+
}
5053
$that = $composed;
5154
} while ($composed instanceof :x:element);
5255

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?hh // strict
2+
/*
3+
* Copyright (c) 2015, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*
10+
*/
11+
12+
/**
13+
* Indicates that any attributes set on an element should be transferred to the
14+
* element returned from ::render() or ::asyncRender(). This is automatically
15+
* invoked by :x:element.
16+
*/
17+
interface XHPHasTransferAttributes {
18+
require extends :x:element;
19+
public function transferAttributesToRenderedRoot(
20+
:x:composable-element $root,
21+
): void;
22+
}

src/html/Element.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
* own elements.
1717
*/
1818
abstract class :xhp:html-element extends :x:primitive {
19-
20-
use XHPHelpers;
19+
use XHPBaseHTMLHelpers;
2120

2221
attribute
2322
// Global HTML attributes

src/html/HasXHPBaseHTMLHelpers.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?hh // strict
2+
/*
3+
* Copyright (c) 2015, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*
10+
*/
11+
12+
interface HasXHPBaseHTMLHelpers {
13+
require extends :x:composable-element;
14+
15+
public function addClass(string $class): this;
16+
public function conditionClass(bool $cond, string $class): this;
17+
public function requireUniqueID(): string;
18+
public function getID(): string;
19+
}

src/html/XHPBaseHTMLHelpers.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?hh // strict
2+
3+
/*
4+
* Copyright (c) 2015, Facebook, Inc.
5+
* All rights reserved.
6+
*
7+
* This source code is licensed under the BSD-style license found in the
8+
* LICENSE file in the root directory of this source tree. An additional grant
9+
* of patent rights can be found in the PATENTS file in the same directory.
10+
*
11+
*/
12+
13+
trait XHPBaseHTMLHelpers implements HasXHPBaseHTMLHelpers {
14+
require extends :x:composable-element;
15+
16+
/*
17+
* Appends a string to the "class" attribute (space separated).
18+
*/
19+
public function addClass(string $class): this {
20+
try {
21+
$current_class = /* UNSAFE_EXPR */ $this->:class;
22+
return $this->setAttribute('class', trim($current_class.' '.$class));
23+
} catch (XHPInvalidAttributeException $error) {
24+
throw new XHPException(
25+
'You are trying to add an HTML class to a(n) '.
26+
:xhp::class2element(static::class).' element, but it does not support '.
27+
'the "class" attribute. The best way to do this is to inherit '.
28+
'the HTML attributes from the element your component will render into.',
29+
);
30+
}
31+
}
32+
33+
/*
34+
* Conditionally adds a class to the "class" attribute.
35+
*/
36+
public function conditionClass(bool $cond, string $class): this {
37+
return $cond ? $this->addClass($class) : $this;
38+
}
39+
40+
/*
41+
* Generates a unique ID (and sets it) on the "id" attribute. A unique ID
42+
* will only be generated if one has not already been set.
43+
*/
44+
public function requireUniqueID(): string {
45+
$id = /* UNSAFE_EXPR */ $this->:id;
46+
if ($id === null || $id === '') {
47+
try {
48+
$this->setAttribute('id', $id = substr(md5(mt_rand(0, 100000)), 0, 10));
49+
} catch (XHPInvalidAttributeException $error) {
50+
throw new XHPException(
51+
'You are trying to add an HTML id to a(n) '.
52+
:xhp::class2element(static::class).' element, but it does not '.
53+
'support the "id" attribute. The best way to do this is to inherit '.
54+
'the HTML attributes from the element your component will render '.
55+
'into.',
56+
);
57+
}
58+
}
59+
return (string)$id;
60+
}
61+
62+
/*
63+
* Fetches the "id" attribute, will generate a unique value if not set.
64+
*/
65+
final public function getID(): string {
66+
return $this->requireUniqueID();
67+
}
68+
}

src/html/XHPHelpers.php

Lines changed: 57 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
*
1010
*/
1111

12-
interface HasXHPHelpers {
13-
require extends :x:composable-element;
12+
interface HasXHPHelpers extends HasXHPBaseHTMLHelpers, XHPHasTransferAttributes {
1413
};
1514

1615
/*
@@ -20,61 +19,9 @@ interface HasXHPHelpers {
2019
* attribute :xhp:html-element;
2120
*/
2221
trait XHPHelpers implements HasXHPHelpers {
23-
2422
require extends :x:composable-element;
2523

26-
/*
27-
* Appends a string to the "class" attribute (space separated).
28-
*/
29-
public function addClass(string $class): this {
30-
try {
31-
$current_class = /* UNSAFE_EXPR */ $this->:class;
32-
return $this->setAttribute('class', trim($current_class.' '.$class));
33-
} catch (XHPInvalidAttributeException $error) {
34-
throw new XHPException(
35-
'You are trying to add an HTML class to a(n) '.
36-
:xhp::class2element(static::class).' element, but it does not support '.
37-
'the "class" attribute. The best way to do this is to inherit '.
38-
'the HTML attributes from the element your component will render into.',
39-
);
40-
}
41-
}
42-
43-
/*
44-
* Conditionally adds a class to the "class" attribute.
45-
*/
46-
public function conditionClass(bool $cond, string $class): this {
47-
return $cond ? $this->addClass($class) : $this;
48-
}
49-
50-
/*
51-
* Generates a unique ID (and sets it) on the "id" attribute. A unique ID
52-
* will only be generated if one has not already been set.
53-
*/
54-
public function requireUniqueID(): string {
55-
$id = /* UNSAFE_EXPR */ $this->:id;
56-
if ($id === null || $id === '') {
57-
try {
58-
$this->setAttribute('id', $id = substr(md5(mt_rand(0, 100000)), 0, 10));
59-
} catch (XHPInvalidAttributeException $error) {
60-
throw new XHPException(
61-
'You are trying to add an HTML id to a(n) '.
62-
:xhp::class2element(static::class).' element, but it does not '.
63-
'support the "id" attribute. The best way to do this is to inherit '.
64-
'the HTML attributes from the element your component will render '.
65-
'into.',
66-
);
67-
}
68-
}
69-
return (string)$id;
70-
}
71-
72-
/*
73-
* Fetches the "id" attribute, will generate a unique value if not set.
74-
*/
75-
final public function getID(): string {
76-
return $this->requireUniqueID();
77-
}
24+
use XHPBaseHTMLHelpers;
7825

7926
/*
8027
* Copies all attributes that are set on $this and valid on $target to
@@ -187,4 +134,59 @@ final private function transferAttributesImpl(
187134
}
188135
}
189136

137+
protected function getAttributeNamesThatAppendValuesOnTransfer(): ImmSet<string> {
138+
return ImmSet { 'class' };
139+
}
140+
141+
final public function transferAttributesToRenderedRoot(
142+
:x:composable-element $root,
143+
): void {
144+
if (:xhp::$ENABLE_VALIDATION && $root instanceof :x:element) {
145+
if (!($root instanceof HasXHPHelpers)) {
146+
throw new XHPClassException(
147+
$this,
148+
'render() must return an object using the XHPHelpers trait.'
149+
);
150+
}
151+
152+
$rootID = $root->getAttribute('id') ?: null;
153+
$thisID = $this->getAttribute('id') ?: null;
154+
155+
if ($rootID && $thisID && $rootID != $thisID) {
156+
throw new XHPException(
157+
'ID Collision. '.(:xhp::class2element(self::class)).' has an ID '.
158+
'of "'.$thisID.'" but it renders into a(n) '.
159+
(:xhp::class2element(get_class($root))).
160+
' which has an ID of "'.$rootID.'". The latter will get '.
161+
'overwritten (most often unexpectedly). If you are intending for '.
162+
'this behavior consider calling $this->removeAttribute(\'id\') '.
163+
'before returning your node from compose().'
164+
);
165+
}
166+
}
167+
assert($root instanceof HasXHPHelpers);
168+
169+
$attributes = $this->getAttributes();
170+
171+
// We want to append classes to the root node, instead of replace them,
172+
// so do this attribute manually and then remove it.
173+
foreach ($this->getAttributeNamesThatAppendValuesOnTransfer() as $attr) {
174+
if (array_key_exists($attr, $attributes)) {
175+
$rootAttributes = $root->getAttributes();
176+
if (
177+
array_key_exists($attr, $rootAttributes)
178+
&& ($rootValue = (string) $rootAttributes[$attr]) !== ''
179+
) {
180+
$thisValue = (string) $attributes[$attr];
181+
if ($thisValue !== '') {
182+
$root->setAttribute($attr, $rootValue.' '.$thisValue);
183+
}
184+
$this->removeAttribute($attr);
185+
}
186+
}
187+
}
188+
189+
// Transfer all valid attributes to the returned node.
190+
$this->transferAllAttributes($root);
191+
}
190192
}

0 commit comments

Comments
 (0)