Jump to content

Splitting field content to parts and showing them in divs


MilenKo
 Share

Recommended Posts

Hello guys. I am almost on the final line of building my second complete website with ProcessWire and got stuck a bit on the recipe nutrition presentation. I am not sure if I would be able to explain it promptly, but will do my best. So I have the code in my HTML template:

Spoiler

<div class="nutrition-detail">
  <div class="left-box">
    Deitary Fiber
    <br>
    <span>50g</span>
  </div>
  <div class="right-box">
    Sodium
    <br>
    <span>10g</span>
  </div>
</div>
<div class="separator-post"></div>
<div class="nutrition-detail">
  <div class="left-box">
    Deitary Fiber
    <br>
    <span>50g</span>
  </div>
  <div class="right-box">
    Sodium
    <br>
    <span>10g</span>
  </div>
</div>
<div class="separator-post"></div>

 

So to fill up the values in the frontend, I am using a simple TextArea field and dropping the following content:

Quote

Calories: 1000 kj
Fats: 35 gr
Cholesterol: 20 gr
Sodium: 14 gr
Carbohydrate: 40 gr
Protein: 60 gr

As far as I needed to split the text field into separate lines and every line into 3 parts (nutrition name, quantity and measure) I used regex code to sort everything through and the results are OK. Now the issue I am facing is that as per the theme, I need to assign class to left or right block and on top of that to close the nutrition-detail div after every right-box div.

For the class I added $counter and using odd:even managed to assign the class of left-box & right-box. But I am having difficulty to insert the opening div of nutrition-detail on the beginning of every row and the closing div after the second block. Here is the code that sort of works, but is not inserting the divs properly:

Spoiler

<?php $text = $sanitizer->textarea($page->recipe_nutrition); // remove markup from body
$nutrition = new WireArray();

// regex pattern to extract values
$pattern =
'/^' . // beginning of each line
'\s*' . // may start with some whitespace
'(?P<name>[^:]+)' . // name (first part until colon)
'\s*:\s*' . // a colon that may be surrounded by whitespaces
'(?P<amount>[\d,\.]+)' . // value (a number that may include , or . like 1,234.56)
'\s*' .  // after the number there may be some whitespace
'(?P<unit>\w+)'. // unit (one or more letters)
'\s*$/'; // there might be some whitespace before the end

$i = 1; //Startup value of counter

foreach (explode("\n", $text) as $line) {
	preg_match($pattern, $line, $parts);
  
	$nutrition->add((new WireData())->setArray([
		'name' => $parts['name'],
		'amount' => floatval($parts['amount']),
		'unit' => $parts['unit']
	]));
}

$order = [
	'Calories',
	'Fats',
	'Saturated',
	'Salt',
	'Carbs',
	'Fibre',
	'Sugar',
	'Proteins',
];

$markup = '';
foreach ($order as $o) {
$item = $nutrition->get("name%=$o");
$class = ($i & 1) ? 'left-box' : 'right-box';

// skip item if its value is not populated
if(empty($item->name)) continue;					
echo "<div class='nutrition-detail'><div class='$class'>$item->name<br><span>$item->amount $item->unit</span></div></div>"

$i++; //Increase the counter with 1 for the odd:even
}
 
?>	

 

Any suggestions how to go around or some simple function to introduce to make it work? I tried to add another check for odd:even and if the result is odd, to insert the <div class="nutrition-detail"> and close the div if even, but I am still missing something...

Link to comment
Share on other sites

40 minutes ago, MilenKo said:

Calories: 1000 kj
Fats: 35 gr
Cholesterol: 20 gr
Sodium: 14 gr
Carbohydrate: 40 gr
Protein: 60 gr

Not what you asked, but assuming that you will be using these (or their variants) nutrition name, quantity and measure over and over, typing them in a textarea field and using some regex magic to split them up is probably not the better solution. In addition, typing the stuff up every time is tedious. I'd go for page fields or fieldtype options, or better yet, since you are grouping items, repeaters or even better repeater matrix or table combined with page field (for nutrition name) + integer (quantity) + fieldtype options or page field (measure). The latter modules are paid modules but well worth the spend.

Using this approach, not only do you throw out the need to use regex, you also get the benefits of a clear presentation, user friendly/easy to edit, easy to add and remove items, searchable items, etc.

Edited by kongondo
  • Like 2
Link to comment
Share on other sites

2 minutes ago, kongondo said:

not only do you throw out the need to use regex, you also get the benefits of a clear presentation, user friendly/easy to edit, easy to add and remove items, searchable items, etc.

@MilenKo If you want to go plain text, you can use: https://modules.processwire.com/modules/fieldtype-yaml/ and no need to use regex magic :) Also, to make it colorful, you can use this one: https://modules.processwire.com/modules/inputfield-ace-extended/

But the latter has some quirks when using with Fieldtype Yaml, I cannot recall exactly what, but it was something with the settings not showing. I decided to remove it because Inputfield Ace Extended's GUI is too scary for ordinary editors :) 

  • Like 1
Link to comment
Share on other sites

@szabesz I must have copied here one of the tests I ran. so $i = 1 and at the end of the loop I am increasing it: $i++; I edited the example code. I thought to give a try of YAML, but the initial idea to get the nutrition into a text field is to simply copy the values from a specific program that we use to generate the totals for a specific group of ingredients.

@kongondo Initially I started with a repeater, but I find it to be too much to add 10-12 values in the repeater. I do use the repeater for the recipe steps where I need to have images, text, step timing etc. and it works great, but for the nutrition I think it is much easier in my case to copy/paste the generated values.

Will see the chessboard examples for a hint. I might also edit the CSS to avoid the insertion of the row div for left and right block. I find it to be much better coded if all the boxes are using the same class and adding two keeps them on the same row, but the third shows on the next one and so fort. This way a simple FOR loop would do the trick super easy. The question is more like if it ever happens to deal with such scenario how to find a suitable working example :)

 

  • Like 1
Link to comment
Share on other sites

@szabesz  I found the idea in your links and made it to work perfectly for my needs so far:

Spoiler

<?php $text = $sanitizer->textarea($page->recipe_nutrition); // remove markup from body
	$nutrition = new WireArray();

	// regex pattern to extract values
	$pattern =
	'/^' . // beginning of each line
	'\s*' . // may start with some whitespace
	'(?P<name>[^:]+)' . // name (first part until colon)
	'\s*:\s*' . // a colon that may be surrounded by whitespaces
	'(?P<amount>[\d,\.]+)' . // value (a number that may include , or . like 1,234.56)
	'\s*' .  // after the number there may be some whitespace
	'(?P<unit>\w+)'. // unit (one or more letters)
	'\s*$/'; // there might be some whitespace before the end
	
	foreach (explode("\n", $text) as $line) {
		preg_match($pattern, $line, $parts);
	  
		$nutrition->add((new WireData())->setArray([
			'name' => $parts['name'],
			'amount' => floatval($parts['amount']),
			'unit' => $parts['unit']
		]));
	}
	
	$order = [
		'Calories',
		'Fats',
		'Saturated',
		'Sodium',
		'Carbs',
		'Fibre',
		'Sugar',
		'Proteins',
	];
	
	$i = 1;

	foreach ($order as $o) {
	$res = count($order);
	$item = $nutrition->get("name%=$o");
	$i++; // 1, 2, 3, 4, 5, etc
		$odd = $i % 2 == 0;

		if ($odd) {
			echo "<div class='nutrition-detail'>"; // Add the starting of row div
			echo "<div class='left-box'>$item->name<br><span>$item->amount $item->unit</span></div>"; //Row with the class for left box
		} else {
			echo "<div class='right-box'>$item->name<br><span>$item->amount $item->unit</span></div>"; //Row with the class for the right box
			echo "</div>"; //Close the nutrition-detail div after the right box
			if ($i < $res) { // Checking if $i is not smaller than the count of results
			echo "<div class='separator-post'></div>"; //If $res is smaller than $i, show the separator div. If not, remove it from the last row
			}
		}    
	}					
?>	

There is only one thing to work on - if a parameter is not added in Tracy debugger it shows an error of trying to get property of non-existing object which in my case should not happen prior to the fact that the info is copied and pasted, but I will revise the other suggested approaches by @kongondo and see which one would fit best.

Thank you again for the ideas, guys. Sometimes we just need a little push to the right direction.

 

  • Like 1
Link to comment
Share on other sites

16 hours ago, MilenKo said:

I found the idea in your links and made it to work perfectly for my needs

I'm glad it could help you!

18 hours ago, MilenKo said:

simply copy the values from a specific program

Yeah, in that case it is probably worth the regexp effort. When someone is not fluent in regular expression (like me :) ) it can be really time consuming to find the right rules if ever... :P But in the case of a reliable and well formatted source it is a lot easier to construct the expressions which work just right.

Link to comment
Share on other sites

20 hours ago, MilenKo said:

But I am having difficulty to insert the opening div of nutrition-detail on the beginning of every row and the closing div after the second block. Here is the code that sort of works, but is not inserting the divs properly:

  Reveal hidden contents


<?php $text = $sanitizer->textarea($page->recipe_nutrition); // remove markup from body
$nutrition = new WireArray();

// regex pattern to extract values
$pattern =
'/^' . // beginning of each line
'\s*' . // may start with some whitespace
'(?P<name>[^:]+)' . // name (first part until colon)
'\s*:\s*' . // a colon that may be surrounded by whitespaces
'(?P<amount>[\d,\.]+)' . // value (a number that may include , or . like 1,234.56)
'\s*' .  // after the number there may be some whitespace
'(?P<unit>\w+)'. // unit (one or more letters)
'\s*$/'; // there might be some whitespace before the end

$i = 1; //Startup value of counter

foreach (explode("\n", $text) as $line) {
	preg_match($pattern, $line, $parts);
  
	$nutrition->add((new WireData())->setArray([
		'name' => $parts['name'],
		'amount' => floatval($parts['amount']),
		'unit' => $parts['unit']
	]));
}

$order = [
	'Calories',
	'Fats',
	'Saturated',
	'Salt',
	'Carbs',
	'Fibre',
	'Sugar',
	'Proteins',
];

$markup = '';
foreach ($order as $o) {
$item = $nutrition->get("name%=$o");
$class = ($i & 1) ? 'left-box' : 'right-box';

// skip item if its value is not populated
if(empty($item->name)) continue;					
echo "<div class='nutrition-detail'><div class='$class'>$item->name<br><span>$item->amount $item->unit</span></div></div>"

$i++; //Increase the counter with 1 for the odd:even
}
 
?>	

 

Any suggestions how to go around or some simple function to introduce to make it work? I tried to add another check for odd:even and if the result is odd, to insert the <div class="nutrition-detail"> and close the div if even, but I am still missing something...

Here you go

<?php


$nutritionMarkup = '';
$i = 0;
foreach ($order as $o) {
    $item = $nutrition->get("name%=$o");
    // skip if item or its name or value not populated
    if (!$item || !$item->name || !$item->value) continue;
    
    // increase the counter and assign left or right
    $class = (($i++) % 2 == 0) ? 'left-box' : 'right-box';

    // to place two nutrition values in a wrapper,
    // open up wrapper before every even item
    if ($i % 2 === 0) $nutritionMarkup .= "<div class='nutrition-detail'>";

    // markup for each nutritional value
    $nutritionMarkup .= "
            <div class='$class'>
                {$item->name}<br>
                {$item->amount} {$item->unit}
            </div>
    ";

    // close the wrapper after every odd item
    if ($i % 2 === 1) $nutritionMarkup .= "</div>";
}

echo $nutritionMarkup;

 

  • Like 1
Link to comment
Share on other sites

@szabesz you are perfectly right about regexp but I found a way to generate it using different sources from Internet and try/error.

@abdus - your code looks much better to understand so I decided to give it a try and use it, however it did not apply the divs properly. I noticed a small typo that was causing the values not to show - in the checks line it was $item->value, so I changed it to $item->unit and it showed the results. There was an issue where the first value was coming with right-box so I had to change $i = 1 and it worked. Besides that, I added the separator div that was not applied and added a check to avoid showing the separator on the last line (as it is in the theme). Here is the final code:

Spoiler

<?php $text = $sanitizer->textarea($page->recipe_nutrition); // remove markup from body
	$nutrition = new WireArray();

	// regex pattern to extract values
	$pattern =
	'/^' . // beginning of each line
	'\s*' . // may start with some whitespace
	'(?P<name>[^:]+)' . // name (first part until colon)
	'\s*:\s*' . // a colon that may be surrounded by whitespaces
	'(?P<amount>[\d,\.]+)' . // value (a number that may include , or . like 1,234.56)
	'\s*' .  // after the number there may be some whitespace
	'(?P<unit>\w+)'. // unit (one or more letters)
	'\s*$/'; // there might be some whitespace before the end
	
	foreach (explode("\n", $text) as $line) {
		preg_match($pattern, $line, $parts);
	  
		$nutrition->add((new WireData())->setArray([
			'name' => $parts['name'],
			'amount' => floatval($parts['amount']),
			'unit' => $parts['unit']
		]));
	}
	
	$order = [
		'Calories',
		'Fats',
		'Saturated',
		'Sodium',
		'Carbs',
		'Fibre',
		'Sugar',
		'Protein',
	];
	
	$nutritionMarkup = '';
	$i = 1;
	foreach ($order as $o) {

		$item = $nutrition->get("name%=$o");
		// skip if item or its name or value not populated
		if (!$item || !$item->name || !$item->unit) continue;
		
		// increase the counter and assign left or right
		$class = (($i++) % 2 == 0) ? 'left-box' : 'right-box';

		// to place two nutrition values in a wrapper,
		// open up wrapper before every even item
		if ($i % 2 === 0) $nutritionMarkup .= "<div class='nutrition-detail'>";

		// markup for each nutritional value
		$nutritionMarkup .= "
				<div class='$class'>
					{$item->name}<br>
					{$item->amount} {$item->unit}
				</div>
		";

		// close the wrapper after every odd item
		if ($i % 2 === 1) $nutritionMarkup .= "</div>";
		if ($i % 2 === 1 && $i < count($order)) $nutritionMarkup .= "<div class='separator-post'></div>";
	}

	echo $nutritionMarkup;
	
?>		

 

Thanks again for the code and idea. Now I can move to the next part...

 

Link to comment
Share on other sites

1 hour ago, MilenKo said:

Besides that, I added the separator div that was not applied and added a check to avoid showing the separator on the last line (as it is in the theme). Here is the final code:

You can use an array and join rows with the separator

<?php


$rows = [];
$i = 0;
foreach ($order as $o) {
    $item = $nutrition->get("name%=$o");
    // skip if item or its name or value not populated
    if (!$item || !$item->name || !$item->amount) continue;

    $row = '';
    // increase the counter and assign left or right
    $class = ($i % 2 === 0) ? 'left-box' : 'right-box';

    // to place two nutrition values in a wrapper,
    // on every even item open up wrapper
    if ($i % 2 === 0) $row .= "<div class='nutrition-detail'>";

    // markup for each nutritional value
    $row .= "
            <div class='$class'>
                {$item->name}<br>
                {$item->amount} {$item->unit}
            </div>
    ";

    // on every odd item, close the wrapper again
    if ($i % 2 === 1) {
        $row .= "</div>";
        $rows[] = $row;
    }

    $i++;
}

$seperator = "<div class='separator-post'></div>";
echo join($seperator, $rows);
Link to comment
Share on other sites

@abdus Thanks for the suggestion and the array approach. I tested out the code and it shows the values, but the markup it returns is not correct and that messes up the appearance.

Here is the original markup:

<div class="nutritional">
	<h3>Nutritional</h3>
	<div class="nutrition-detail">
		<div class="left-box">
			Protine
			<br>
			<span>6.60g</span>
		</div>
		<div class="right-box">
			Fat Saturated
			<br>
			<span>39.5g</span>
		</div>
	</div>
	<div class="separator-post"></div>
	<div class="nutrition-detail">
		<div class="left-box">
			Deitary Fiber
			<br>
			<span>50g</span>
		</div>
		<div class="right-box">
			Sodium
			<br>
			<span>10g</span>
		</div>
	</div>
	<div class="separator-post"></div>

</div>

And here is what the array code generates:

<div class="nutritional">
        <h3>Nutritional Info</h3>			
			<div class="right-box">
				Calories<br>
				1000 kj
			</div>
		</div>
		
		<div class="separator-post"></div>
		
		<div class="right-box">
					Saturated<br>
					2 gr
				</div>
		</div>
		<div class="separator-post"></div>

The way I see it, it does not insert nutrition-detail div at all as well as does not apply class left-box. Also it inserts a </div> on every odd row that is closing the main "nutritional" div and moves the rest out of it. I will play with the code to see where it goes wrong :)

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...